mirror of
https://gitee.com/zhangsken/aliyunpan.git
synced 2026-10-16 10:29:42 +08:00
Compare commits
75 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d061a28559 | |||
| e91e66aadf | |||
| 71f55424d0 | |||
| c6c4d871ef | |||
| 50e6200607 | |||
| c363d90e72 | |||
| 048a6a5438 | |||
| 19afa16bfe | |||
| b559f41e47 | |||
| 33a5484e4a | |||
| 34254bf822 | |||
| 66540c9457 | |||
| c522b530d7 | |||
| 3d130949c0 | |||
| 1365471639 | |||
| 151574e154 | |||
| 55e989d821 | |||
| 0edf7d0af2 | |||
| ccc285ce5e | |||
| 9992ffb3bc | |||
| 25d916b744 | |||
| 471ee93417 | |||
| 78e290c2a4 | |||
| 23fe54ce0b | |||
| 8d2c40f1ce | |||
| fa144a45a1 | |||
| 2740a15cbf | |||
| 3be79d1181 | |||
| ab215736a9 | |||
| 45877a4408 | |||
| 50a05a6bec | |||
| e33d23831a | |||
| cbf372bde0 | |||
| 8d14617397 | |||
| 12d866b28a | |||
| 85d79a9657 | |||
| 828c1fe39a | |||
| c5a5cb906d | |||
| 4ba61c19c8 | |||
| 571baa1482 | |||
| 4575dfdaa8 | |||
| efbf0b7bdb | |||
| 416c5cd57e | |||
| e0e8bc3284 | |||
| 5b22c4400a | |||
| c9c33217d2 | |||
| 79dcf40792 | |||
| d618686da9 | |||
| 5f52145378 | |||
| bd958fb8df | |||
| 8fc8d9b6f7 | |||
| 70ddb7e0c1 | |||
| 0acd6a43d6 | |||
| 97a22812a2 | |||
| 118e521d63 | |||
| f8c4961608 | |||
| ed38002e65 | |||
| b74d39da41 | |||
| aabab869c4 | |||
| bb8dad7262 | |||
| 9f9fb6a320 | |||
| 84d7ec923f | |||
| 07746fc15c | |||
| c5d15b058a | |||
| ea9279e45e | |||
| 6abab46db0 | |||
| 6e8ee94285 | |||
| 1208ccad08 | |||
| d23802e8cc | |||
| 59646b07fe | |||
| 34dd8a98fe | |||
| b6780931cc | |||
| fb705365b1 | |||
| 1c6f6c772c | |||
| 7726888365 |
@@ -1,40 +1,46 @@
|
||||
# 关于
|
||||
阿里云盘CLI。仿 Linux shell 文件处理命令的阿里云盘命令行客户端,支持JavaScript插件,支持webdav文件协议,支持同步备份功能。
|
||||
阿里云盘CLI。仿 Linux shell 文件处理命令的阿里云盘命令行客户端,支持JavaScript插件,支持同步备份功能。
|
||||
|
||||
# 特色
|
||||
1. 多平台支持, 支持 Windows, macOS, linux(x86/x64/arm), android, iOS 等
|
||||
2. 阿里云盘多用户支持
|
||||
3. 支持文件网盘,相册网盘无缝切换
|
||||
3. 支持同步盘,资源库,相册网盘无缝切换
|
||||
4. [下载](docs/manual.md#下载文件目录)网盘内文件, 支持多个文件或目录下载, 支持断点续传和单文件并行下载。支持软链接(符号链接)文件。
|
||||
5. [上传](docs/manual.md#上传文件目录)本地文件, 支持多个文件或目录上传,支持排除指定文件夹/文件(正则表达式)功能。支持软链接(符号链接)文件。
|
||||
6. [同步备份功能](docs/manual.md#同步备份功能)支持备份本地文件到云盘,备份云盘文件到本地,双向同步备份保持本地文件和网盘文件同步。常用于嵌入式或者NAS等设备,支持docker镜像部署。
|
||||
7. 命令和文件路径输入支持Tab键自动补全,路径支持通配符匹配模式
|
||||
8. 支持阿里云ECS环境下使用内网链接上传/下载,速度更快(只支持阿里经典网络,最高可达100MB/s),还可以节省公网带宽流量(配置transfer_url_type=2即可)
|
||||
9. 支持[webdav文件服务](docs/manual.md#webdav文件服务),可以将阿里云盘当做webdav文件网盘挂载到Windows, macOS, linux的磁盘中进行使用。webdav部署支持docker镜像,镜像只有不到10MB非常小巧。
|
||||
10. 支持[JavaScript插件](docs/manual.md#JavaScript插件),你可以按照自己的需要定制上传/下载中关键步骤的行为,最大程度满足自己的个性化需求
|
||||
9. 支持[JavaScript插件](docs/manual.md#JavaScript插件),你可以按照自己的需要定制上传/下载中关键步骤的行为,最大程度满足自己的个性化需求
|
||||
|
||||
# 目录
|
||||
- [关于](#关于)
|
||||
- [特色](#特色)
|
||||
- [目录](#目录)
|
||||
- [如何安装](#如何安装)
|
||||
* [直接下载安装](#直接下载安装)
|
||||
* [apt安装](#apt安装)
|
||||
* [yum安装](#yum安装)
|
||||
* [docker安装](#docker安装)
|
||||
+ [sync同步盘](#sync同步盘)
|
||||
+ [webdav共享盘](#webdav共享盘)
|
||||
- [直接下载安装](#直接下载安装)
|
||||
- [apt安装](#apt安装)
|
||||
- [yum安装](#yum安装)
|
||||
- [brew安装](#brew安装)
|
||||
- [winget安装](#winget安装)
|
||||
- [docker安装](#docker安装)
|
||||
- [sync同步盘](#sync同步盘)
|
||||
- [如何使用](#如何使用)
|
||||
* [基本使用](#基本使用)
|
||||
+ [修改配置目录](#修改配置目录)
|
||||
+ [启动程序](#启动程序)
|
||||
+ [查看帮助](#查看帮助)
|
||||
+ [登录](#登录)
|
||||
+ [查看文件列表](#查看文件列表)
|
||||
+ [下载文件](#下载文件)
|
||||
+ [上传文件](#上传文件)
|
||||
+ [同步备份文件](#同步备份文件)
|
||||
* [更多命令](#更多命令)
|
||||
- [基本使用](#基本使用)
|
||||
- [修改配置目录](#修改配置目录)
|
||||
- [启动程序](#启动程序)
|
||||
- [查看帮助](#查看帮助)
|
||||
- [登录](#登录)
|
||||
- [查看文件列表](#查看文件列表)
|
||||
- [下载文件](#下载文件)
|
||||
- [上传文件](#上传文件)
|
||||
- [同步备份文件](#同步备份文件)
|
||||
- [更多命令](#更多命令)
|
||||
- [常见问题](#常见问题)
|
||||
* [如何获取RefreshToken](#如何获取RefreshToken)
|
||||
* [如何开启Debug调试日志](#如何开启Debug调试日志)
|
||||
- [如何获取RefreshToken](#如何获取refreshtoken)
|
||||
- [如何开启Debug调试日志](#如何开启debug调试日志)
|
||||
- [第一步](#第一步)
|
||||
- [第二步](#第二步)
|
||||
- [如何登出和下线客户端](#如何登出和下线客户端)
|
||||
- [交流反馈](#交流反馈)
|
||||
- [鸣谢](#鸣谢)
|
||||
|
||||
@@ -53,9 +59,9 @@
|
||||
|
||||
参考例子:
|
||||
```shell
|
||||
wget https://github.com/tickstep/aliyunpan/releases/download/v0.2.4/aliyunpan-v0.2.4-linux-amd64.zip
|
||||
unzip aliyunpan-v0.2.4-linux-amd64.zip
|
||||
cd aliyunpan-v0.2.4-linux-amd64
|
||||
wget https://github.com/tickstep/aliyunpan/releases/download/v0.2.8/aliyunpan-v0.2.8-linux-amd64.zip
|
||||
unzip aliyunpan-v0.2.8-linux-amd64.zip
|
||||
cd aliyunpan-v0.2.8-linux-amd64
|
||||
./aliyunpan
|
||||
```
|
||||
|
||||
@@ -73,6 +79,31 @@ sudo curl -fsSL http://file.tickstep.com/rpm/aliyunpan/aliyunpan.repo | sudo tee
|
||||
|
||||
```
|
||||
|
||||
## brew安装
|
||||
适用于brew包管理器的系统,主要是苹果macOS系统。目前只支持amd64和arm64架构(Apple Silicon)的机器。
|
||||
```shell
|
||||
brew install aliyunpan
|
||||
|
||||
```
|
||||
由于brew默认安装在系统目录下面,这样配置文件也默认存放在系统目录里了,建议设置系统变量进行配置文件的单独存储,例如
|
||||
```shell
|
||||
export ALIYUNPAN_CONFIG_DIR=/Users/tickstep/Applications/adrive/config
|
||||
```
|
||||
|
||||
## winget安装
|
||||
适用于Windows系统的winget包管理器。目前只支持x86和x64架构的机器。
|
||||
|
||||
更新源(可选)
|
||||
```powershell
|
||||
winget source update
|
||||
|
||||
```
|
||||
安装
|
||||
```powershell
|
||||
winget install tickstep.aliyunpan --silent
|
||||
|
||||
```
|
||||
|
||||
## docker安装
|
||||
### sync同步盘
|
||||
同步备份功能,支持备份本地文件到云盘,备份云盘文件到本地,双向同步备份三种模式。支持JavaScript插件对备份文件进行过滤。
|
||||
@@ -81,7 +112,7 @@ sudo curl -fsSL http://file.tickstep.com/rpm/aliyunpan/aliyunpan.repo | sudo tee
|
||||
2. 备份云盘文件,即下载网盘文件到本地,始终保持网盘的文件有一个完整的备份在本地
|
||||
3. 双向备份,保持网盘文件和本地文件严格一致
|
||||
```
|
||||
docker run -d --name=aliyunpan-sync --restart=always -v "<your local dir>:/home/app/data" -e TZ="Asia/Shanghai" -e ALIYUNPAN_REFRESH_TOKEN="<your refreshToken>" -e ALIYUNPAN_PAN_DIR="<your drive pan dir>" -e ALIYUNPAN_SYNC_MODE="upload" -e ALIYUNPAN_TASK_STEP="sync" tickstep/aliyunpan-sync:v0.2.4
|
||||
docker run -d --name=aliyunpan-sync --restart=always -v "<your local dir>:/home/app/data" -e TZ="Asia/Shanghai" -e ALIYUNPAN_REFRESH_TOKEN="<your refreshToken>" -e ALIYUNPAN_PAN_DIR="<your drive pan dir>" -e ALIYUNPAN_SYNC_MODE="upload" -e ALIYUNPAN_TASK_STEP="sync" tickstep/aliyunpan-sync:v0.2.8
|
||||
|
||||
|
||||
<your local dir>:本地目录绝对路径,例如:/tickstep/Documents/设计文档
|
||||
@@ -92,21 +123,6 @@ ALIYUNPAN_TASK_STEP:任务步骤, 支持两种: scan(只扫描并建立同步
|
||||
```
|
||||
更详情文档请参考dockerhub网址:[tickstep/aliyunpan-sync](https://hub.docker.com/r/tickstep/aliyunpan-sync)
|
||||
|
||||
### webdav共享盘
|
||||
让阿里云盘变身为webdav协议的文件服务器。这样使用webdav客户端软件,你可以把阿里云盘挂载为Windows、Linux、Mac系统的磁盘,可以通过NAS系统做文件管理或文件同步等等。
|
||||
```
|
||||
docker run -d --name=aliyunpan-webdav --restart=always -p 23077:23077 -e TZ="Asia/Shanghai" -e ALIYUNPAN_REFRESH_TOKEN="<your refreshToken>" -e ALIYUNPAN_AUTH_USER="admin" -e ALIYUNPAN_AUTH_PASSWORD="admin" -e ALIYUNPAN_WEBDAV_MODE="rw" -e ALIYUNPAN_PAN_DRIVE="File" -e ALIYUNPAN_PAN_DIR="/" tickstep/aliyunpan-webdav:v0.2.4
|
||||
|
||||
|
||||
ALIYUNPAN_REFRESH_TOKEN RefreshToken
|
||||
ALIYUNPAN_AUTH_USER webdav登录用户名
|
||||
ALIYUNPAN_AUTH_PASSWORD webdav登录密码
|
||||
ALIYUNPAN_WEBDAV_MODE webdav模式,支持:rw-读写,ro-只读
|
||||
ALIYUNPAN_PAN_DRIVE 网盘类型,可选: File-文件 Album-相册
|
||||
ALIYUNPAN_PAN_DIR 网盘文件夹的webdav服务根目录
|
||||
```
|
||||
更详情文档请参考dockerhub网址:[tickstep/aliyunpan-webdav](https://hub.docker.com/r/tickstep/aliyunpan-webdav)
|
||||
|
||||
# 如何使用
|
||||
完整和详细的命令说明请查看手册:[命令手册](docs/manual.md)
|
||||
|
||||
@@ -164,7 +180,6 @@ aliyunpan > help
|
||||
share 分享文件/目录
|
||||
sync 同步备份功能
|
||||
upload, u 上传文件/目录
|
||||
webdav 在线网盘服务
|
||||
...
|
||||
```
|
||||
|
||||
@@ -307,6 +322,15 @@ Windows
|
||||
打开aliyunpan命令行程序,任何云盘命令都有类似如下日志输出
|
||||

|
||||
|
||||
## 如何登出和下线客户端
|
||||
阿里云盘单账户最多只允许同时登录 10 台设备
|
||||
当出现这个提示:你账号已超出最大登录设备数量,请先下线一台设备,然后重启本应用,才可以继续使用
|
||||
说明你的账号登录客户端已经超过数量,你需要先登出其他客户端才能继续使用,如下所示
|
||||
APP手机端
|
||||

|
||||
Web网页端
|
||||

|
||||
|
||||
# 交流反馈
|
||||
提交issue: [issues页面](https://github.com/tickstep/aliyunpan/issues)
|
||||
联系邮箱: tickstep@outlook.com
|
||||
@@ -314,4 +338,5 @@ Windows
|
||||
# 鸣谢
|
||||
本项目大量借鉴了以下相关项目的功能&成果
|
||||
> [tickstep/cloudpan189-go](https://github.com/tickstep/cloudpan189-go)
|
||||
> [hacdias/webdav](https://github.com/hacdias/webdav)
|
||||
> [hacdias/webdav](https://github.com/hacdias/webdav)
|
||||
> [kazutoiris/ali_ecc](https://github.com/kazutoiris/ali_ecc)
|
||||
@@ -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.2.4.0" processorArchitecture="*" name="com.tickstep.aliyunpan" type="win32"/>
|
||||
<assemblyIdentity version="0.2.8.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="*"/>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 371 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 309 KiB |
@@ -119,6 +119,9 @@ Build $name-$version"-windows-arm" windows arm
|
||||
Build $name-$version"-linux-386" linux 386
|
||||
Build $name-$version"-linux-amd64" linux amd64
|
||||
|
||||
# 龙芯 LoongArch
|
||||
Build $name-$version"-linux-loong64" linux loong64
|
||||
|
||||
# arm
|
||||
Build $name-$version"-linux-armv5" linux arm 5
|
||||
Build $name-$version"-linux-armv7" linux arm 7
|
||||
|
||||
@@ -1,39 +1,11 @@
|
||||
package cmder
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdliner"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/functions/panlogin"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/urfave/cli"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
appInstance *cli.App
|
||||
|
||||
saveConfigMutex *sync.Mutex = new(sync.Mutex)
|
||||
|
||||
ReloadConfigFunc = func(c *cli.Context) error {
|
||||
err := config.Config.Reload()
|
||||
if err != nil {
|
||||
fmt.Printf("重载配置错误: %s\n", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
SaveConfigFunc = func(c *cli.Context) error {
|
||||
saveConfigMutex.Lock()
|
||||
defer saveConfigMutex.Unlock()
|
||||
err := config.Config.Save()
|
||||
if err != nil {
|
||||
fmt.Printf("保存配置错误: %s\n", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
)
|
||||
|
||||
func SetApp(app *cli.App) {
|
||||
@@ -43,73 +15,3 @@ func SetApp(app *cli.App) {
|
||||
func App() *cli.App {
|
||||
return appInstance
|
||||
}
|
||||
|
||||
func DoLoginHelper(refreshToken string) (refreshTokenStr string, webToken aliyunpan.WebLoginToken, error error) {
|
||||
line := cmdliner.NewLiner()
|
||||
defer line.Close()
|
||||
|
||||
if refreshToken == "" {
|
||||
refreshToken, error = line.State.Prompt("请输入RefreshToken, 回车键提交 > ")
|
||||
if error != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// app login
|
||||
atoken, apperr := aliyunpan.GetAccessTokenFromRefreshToken(refreshToken)
|
||||
if apperr != nil {
|
||||
if apperr.Code == apierror.ApiCodeTokenExpiredCode || apperr.Code == apierror.ApiCodeRefreshTokenExpiredCode {
|
||||
fmt.Println("Token过期,需要重新登录")
|
||||
} else {
|
||||
fmt.Println("Token登录失败:", apperr)
|
||||
}
|
||||
return "", webToken, fmt.Errorf("登录失败")
|
||||
}
|
||||
refreshTokenStr = refreshToken
|
||||
return refreshTokenStr, *atoken, nil
|
||||
}
|
||||
|
||||
func TryLogin() *config.PanUser {
|
||||
// can do automatically login?
|
||||
for _, u := range config.Config.UserList {
|
||||
if u.UserId == config.Config.ActiveUID {
|
||||
// login
|
||||
_, webToken, err := DoLoginHelper(u.RefreshToken)
|
||||
if err != nil {
|
||||
logger.Verboseln("automatically login use saved refresh token error ", err)
|
||||
if u.TokenId != "" {
|
||||
logger.Verboseln("try to login use tokenId")
|
||||
h := panlogin.NewLoginHelper(config.DefaultTokenServiceWebHost)
|
||||
r, e := h.GetRefreshToken(u.TokenId)
|
||||
if e != nil {
|
||||
logger.Verboseln("try to login use tokenId error", e)
|
||||
break
|
||||
}
|
||||
refreshToken, e := h.ParseSecureRefreshToken("", r.SecureRefreshToken)
|
||||
if e != nil {
|
||||
logger.Verboseln("try to parse refresh token error", e)
|
||||
break
|
||||
}
|
||||
_, webToken, err = DoLoginHelper(refreshToken)
|
||||
if err != nil {
|
||||
logger.Verboseln("try to use refresh token from tokenId error", e)
|
||||
break
|
||||
}
|
||||
fmt.Println("Token重新自动登录成功")
|
||||
// save new refresh token
|
||||
u.RefreshToken = refreshToken
|
||||
}
|
||||
break
|
||||
}
|
||||
// success
|
||||
u.WebToken = webToken
|
||||
|
||||
// save
|
||||
SaveConfigFunc(nil)
|
||||
// reload
|
||||
ReloadConfigFunc(nil)
|
||||
return config.Config.ActiveUser()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ ENV ALIYUNPAN_SYNC_PRIORITY=time
|
||||
ENV ALIYUNPAN_SYNC_LOG=false
|
||||
ENV ALIYUNPAN_LOCAL_DELAY_TIME=3
|
||||
ENV ALIYUNPAN_TASK_STEP=sync
|
||||
ENV ALIYUNPAN_DEVICE_ID=""
|
||||
|
||||
# 运行
|
||||
ENTRYPOINT ./app.sh
|
||||
+18
-1
@@ -29,6 +29,15 @@ fi
|
||||
|
||||
sleep 2s
|
||||
|
||||
# device-id
|
||||
if [[ -z $ALIYUNPAN_DEVICE_ID ]];
|
||||
then
|
||||
echo "the program use random device id"
|
||||
else
|
||||
echo "set device id"
|
||||
./aliyunpan config set -device_id ${ALIYUNPAN_DEVICE_ID}
|
||||
fi
|
||||
|
||||
# check login already or not
|
||||
./aliyunpan who
|
||||
if [ $? -eq 0 ]
|
||||
@@ -39,5 +48,13 @@ else
|
||||
./aliyunpan login -RefreshToken=${ALIYUNPAN_REFRESH_TOKEN}
|
||||
fi
|
||||
|
||||
./aliyunpan config set -transfer_url_type ${ALIYUNPAN_TRANSFER_URL_TYPE}
|
||||
./aliyunpan config set -transfer_url_type ${ALIYUNPAN_TRANSFER_URL_TYPE}
|
||||
|
||||
if [ "$ALIYUNPAN_SYNC_LOG" = "true" ]
|
||||
then
|
||||
./aliyunpan config set -file_record_config 1
|
||||
else
|
||||
./aliyunpan config set -file_record_config 2
|
||||
fi
|
||||
|
||||
./aliyunpan sync start -dp ${ALIYUNPAN_DOWNLOAD_PARALLEL} -up ${ALIYUNPAN_UPLOAD_PARALLEL} -dbs ${ALIYUNPAN_DOWNLOAD_BLOCK_SIZE} -ubs ${ALIYUNPAN_UPLOAD_BLOCK_SIZE} -log ${ALIYUNPAN_SYNC_LOG} -ldt ${ALIYUNPAN_LOCAL_DELAY_TIME} -step ${ALIYUNPAN_TASK_STEP}
|
||||
|
||||
@@ -37,3 +37,5 @@ services:
|
||||
- ALIYUNPAN_LOCAL_DELAY_TIME=3
|
||||
# 任务步骤, 支持两种: scan(只扫描并建立同步数据库),sync(正常启动同步任务)
|
||||
- ALIYUNPAN_TASK_STEP=sync
|
||||
# 登录设备的ID,默认为空,程序会自动生成一个ID
|
||||
- ALIYUNPAN_DEVICE_ID=
|
||||
@@ -54,5 +54,6 @@ ENV ALIYUNPAN_PAN_DIR=/
|
||||
ENV ALIYUNPAN_REFRESH_TOKEN=""
|
||||
ENV ALIYUNPAN_TRANSFER_URL_TYPE=0
|
||||
ENV ALIYUNPAN_BLOCK_SIZE=10240
|
||||
ENV ALIYUNPAN_DEVICE_ID=""
|
||||
|
||||
ENTRYPOINT ./app.sh
|
||||
@@ -2,6 +2,15 @@
|
||||
cd /home/app
|
||||
chmod +x ./aliyunpan
|
||||
|
||||
# device-id
|
||||
if [[ -z $ALIYUNPAN_DEVICE_ID ]];
|
||||
then
|
||||
echo "the program use random device id"
|
||||
else
|
||||
echo "set device id"
|
||||
./aliyunpan config set -device_id ${ALIYUNPAN_DEVICE_ID}
|
||||
fi
|
||||
|
||||
# check login already or not
|
||||
./aliyunpan who
|
||||
if [ $? -eq 0 ]
|
||||
|
||||
@@ -25,7 +25,8 @@ services:
|
||||
- ALIYUNPAN_TRANSFER_URL_TYPE=1
|
||||
# 上传数据块大小,单位为KB,默认为10240KB,建议范围1024KB~10240KB
|
||||
- ALIYUNPAN_BLOCK_SIZE=10240
|
||||
|
||||
# 登录设备的ID,默认为空,程序会自动生成一个ID
|
||||
- ALIYUNPAN_DEVICE_ID=
|
||||
|
||||
|
||||
|
||||
|
||||
+61
-232
@@ -10,21 +10,18 @@
|
||||
* [切换阿里云盘帐号](#切换阿里云盘帐号)
|
||||
* [退出阿里云盘帐号](#退出阿里云盘帐号)
|
||||
* [刷新Token](#刷新Token)
|
||||
* [切换网盘(文件/相册)](#切换网盘)
|
||||
* [切换网盘(备份盘/资源库/相册)](#切换网盘)
|
||||
* [获取网盘配额](#获取网盘配额)
|
||||
* [切换工作目录](#切换工作目录)
|
||||
* [输出工作目录](#输出工作目录)
|
||||
* [列出目录](#列出目录)
|
||||
* [下载文件/目录](#下载文件目录)
|
||||
* [获取文件下载直链](#获取文件下载直链)
|
||||
* [上传文件/目录](#上传文件目录)
|
||||
* [手动秒传文件](#手动秒传文件)
|
||||
* [创建目录](#创建目录)
|
||||
* [删除文件/目录](#删除文件目录)
|
||||
* [移动文件/目录](#移动文件目录)
|
||||
* [备份盘和资源库之间转存文件](#备份盘和资源库之间转存文件)
|
||||
* [重命名文件/目录](#重命名文件目录)
|
||||
* [导出文件](#导出文件)
|
||||
* [导入文件](#导入文件)
|
||||
* [分享文件/目录](#分享文件目录)
|
||||
+ [设置分享文件/目录](#设置分享文件目录)
|
||||
+ [列出已分享文件/目录](#列出已分享文件目录)
|
||||
@@ -37,12 +34,6 @@
|
||||
+ [Linux后台启动](#Linux后台启动)
|
||||
+ [Windows后台启动](#Windows后台启动)
|
||||
+ [Docker运行](#Docker运行)
|
||||
* [webdav文件服务](#webdav文件服务)
|
||||
+ [常用命令说明](#常用命令说明)
|
||||
+ [命令行启动](#命令行启动)
|
||||
+ [Linux后台启动](#Linux后台启动)
|
||||
+ [Docker运行](#Docker运行)
|
||||
+ [HTTPS配置](#HTTPS配置)
|
||||
* [JavaScript插件](#JavaScript插件)
|
||||
+ [如何使用](#如何使用)
|
||||
+ [JS中内置的函数](#JS中内置的函数)
|
||||
@@ -56,8 +47,7 @@
|
||||
- [常见问题Q&A](#常见问题Q&A)
|
||||
* [1. 如何获取RefreshToken](#1-如何获取RefreshToken)
|
||||
* [2. 如何开启Debug调试日志](#2-如何开启Debug调试日志)
|
||||
* [3. 解决 time: missing Location in call to Date 问题](#3-解决-missing-Location-in-call-to-Date-问题)
|
||||
|
||||
|
||||
# 命令列表及说明
|
||||
## 注意
|
||||
|
||||
@@ -282,25 +272,6 @@ aliyunpan d /我的文档
|
||||
|
||||
自动跳过下载重名的文件!
|
||||
|
||||
## 获取文件下载直链
|
||||
你可以通过本命令获取文件下载直链,这样你就可以使用其他支持的工具进行下载了。
|
||||
|
||||
***注意***
|
||||
1. 必须二维码登录!必须二维码登录!必须二维码登录!你必须使用手机扫码二维码方式登录(命令:login -QrCode),否则获取的直链无法正常下载,会提示 403 Forbidden 下载被禁止
|
||||
2. 下载链接有效时间为4个小时,当然过期后你可以重新运行命令生成一个新的下载链接
|
||||
3. 暂只支持文件,不支持文件夹
|
||||
|
||||
```
|
||||
aliyunpan locate <文件1> <文件2> <文件3> ...
|
||||
```
|
||||
|
||||
### 例子
|
||||
```
|
||||
获取 /我的资源/1.mp4 下载直链
|
||||
aliyunpan locate /我的资源/1.mp4
|
||||
```
|
||||
|
||||
|
||||
## 上传文件/目录
|
||||
上传支持两种链接类型:1-默认类型 2-阿里ECS环境类型
|
||||
在阿里ECS(必须是"经典网络"类型的机器)环境下,上传速度单文件可以轻松达到30MB/s,多文件可以达到100MB/s
|
||||
@@ -400,6 +371,25 @@ 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 <旧文件/目录名> <新文件/目录名>
|
||||
@@ -413,46 +403,6 @@ aliyunpan rename <旧文件/目录名> <新文件/目录名>
|
||||
aliyunpan rename /我的文档/1.mp4 /我的文档/2.mp4
|
||||
```
|
||||
|
||||
## 导出文件
|
||||
导出文件主要是用于备份网盘的文件。通过该命令将网盘的文件元数据信息导出并保存到本地文件中,等到以后需要的时候再通过import命令导入到网盘中。
|
||||
```
|
||||
aliyunpan export <网盘文件/目录的路径1> <文件/目录2> <文件/目录3> ... <本地保存文件路径>
|
||||
```
|
||||
导出指定文件/目录下面的所有文件的元数据信息,并保存到指定的本地文件里面。导出的文件元信息可以使用 import 命令(秒传文件功能)导入到网盘中。
|
||||
|
||||
### 例子
|
||||
```
|
||||
导出 /我的资源 整个目录 元数据到文件 /Users/tickstep/Downloads/export_files.txt
|
||||
aliyunpan export /我的资源 /Users/tickstep/Downloads/export_files.txt
|
||||
|
||||
导出 网盘 整个目录 元数据到文件 /Users/tickstep/Downloads/export_files.txt
|
||||
aliyunpan export / /Users/tickstep/Downloads/export_files.txt
|
||||
```
|
||||
|
||||
## 导入文件
|
||||
```
|
||||
aliyunpan export <本地元数据文件路径>
|
||||
```
|
||||
导入文件中记录的元数据文件到网盘。保存到网盘的文件会使用文件元数据记录的路径位置,如果没有指定云盘目录(saveto)则默认导入到目录 aliyunpan 中。
|
||||
导入的文件可以使用 export 命令获得。
|
||||
|
||||
导入文件每一行是一个文件元数据,样例如下:
|
||||
```
|
||||
aliyunpan://file.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|
|
||||
```
|
||||
|
||||
### 例子
|
||||
```
|
||||
导入文件 /Users/tickstep/Downloads/export_files.txt 存储的所有文件元数据项
|
||||
aliyunpan import /Users/tickstep/Downloads/export_files.txt
|
||||
|
||||
导入文件 /Users/tickstep/Downloads/export_files.txt 存储的所有文件元数据项并保存到目录 /my2020 中
|
||||
aliyunpan import -saveto=/my2020 /Users/tickstep/Downloads/export_files.txt
|
||||
|
||||
导入文件 /Users/tickstep/Downloads/export_files.txt 存储的所有文件元数据项并保存到网盘根目录 / 中
|
||||
aliyunpan import -saveto=/ /Users/tickstep/Downloads/export_files.txt
|
||||
```
|
||||
|
||||
## 分享文件/目录
|
||||
```
|
||||
aliyunpan share
|
||||
@@ -890,154 +840,6 @@ function syncScanPanFilePrepareCallback(context, params) {
|
||||
}
|
||||
```
|
||||
|
||||
## webdav文件服务
|
||||
本文命令可以让阿里云盘变身为webdav协议的文件服务器。这样你可以把阿里云盘挂载为Windows、Linux、Mac系统的磁盘,可以通过NAS系统做文件管理或文件同步等等。
|
||||
当把阿里云盘作为webdav文件服务器进行使用的时候,上传文件是不支持秒传的,所以当你挂载为网络磁盘使用的时候,不建议在webdav挂载目录中上传、下载过大的文件,不然体验会非常差。
|
||||
建议作为文档,图片等小文件的同步网盘。
|
||||
效果图如下所示:
|
||||

|
||||
|
||||
### 常用命令说明
|
||||
```
|
||||
查看webdav说明
|
||||
aliyunpan webdav
|
||||
|
||||
查看webdav如何启动说明
|
||||
aliyunpan webdav start -h
|
||||
|
||||
使用默认配置启动webdav服务。
|
||||
aliyunpan webdav start
|
||||
|
||||
启动webdav服务,并配置IP为127.0.0.1,端口为23077,登录用户名为admin,登录密码为admin123,文件网盘目录 /webdav_folder 作为服务的根目录
|
||||
aliyunpan webdav start -ip "127.0.0.1" -port 23077 -webdav_user "admin" -webdav_password "admin123" -pan_drive "File" -pan_dir_path "/webdav_folder"
|
||||
|
||||
正常启动后会打印出webdav链接参数,然后使用支持webdav的客户端填入下面对应的参数进行链接即可
|
||||
----------------------------------------
|
||||
webdav网盘信息:
|
||||
链接:http://localhost:23077
|
||||
用户名:admin
|
||||
密码:admin123
|
||||
网盘服务类型:文件
|
||||
网盘服务目录:/webdav_folder
|
||||
----------------------------------------
|
||||
```
|
||||
|
||||
### 命令行启动
|
||||
需要先进行登录。然后使用以下命令运行即可,该命令是阻塞的不会退出,除非停止webdav服务。
|
||||
|
||||
```
|
||||
./aliyunpan webdav start -port 23077 -webdav_user "admin" -webdav_password "admin123" -pan_dir_path "/webdav_folder"
|
||||
|
||||
参数说明
|
||||
port:绑定端口
|
||||
webdav_user: webdav客户端登录用户名
|
||||
webdav_password: webdav客户端登录密码
|
||||
pan_dir_path:指定webdav使用那个阿里云盘目录作为服务根目录
|
||||
```
|
||||
|
||||
### Linux后台启动
|
||||
建议结合nohup进行启动。
|
||||
|
||||
先创建webdav.sh脚本,内容如下
|
||||
```
|
||||
# 请更改成你自己的目录
|
||||
cd /path/to/aliyunpan/folder
|
||||
|
||||
chmod +x ./aliyunpan
|
||||
|
||||
# 指定refresh token用于登录
|
||||
./aliyunpan login -RefreshToken=9078907....adg9087
|
||||
|
||||
# 上传下载链接类型:1-默认 2-阿里ECS环境
|
||||
./aliyunpan config set -transfer_url_type 1
|
||||
|
||||
# 指定webdav启动参数并进行启动
|
||||
./aliyunpan webdav start -ip "0.0.0.0" -port 23077 -webdav_user "admin" -webdav_password "admin" -pan_dir_path "/" -bs 1024
|
||||
```
|
||||
|
||||
增加脚本执行权限
|
||||
```
|
||||
$ chmod +x webdav.sh
|
||||
```
|
||||
|
||||
然后启动该脚本进行后台运行
|
||||
```
|
||||
$ nohup ./webdav.sh >/dev/null 2>&1 &
|
||||
```
|
||||
|
||||
### Docker运行
|
||||
详情文档请参考dockerhub网址:[tickstep/aliyunpan-webdav](https://hub.docker.com/r/tickstep/aliyunpan-webdav)
|
||||
|
||||
1. 直接运行
|
||||
```
|
||||
docker run -d --name=aliyunpan-webdav --restart=always -p 23077:23077 -e TZ="Asia/Shanghai" -e ALIYUNPAN_REFRESH_TOKEN="<your refreshToken>" -e ALIYUNPAN_AUTH_USER="admin" -e ALIYUNPAN_AUTH_PASSWORD="admin" -e ALIYUNPAN_PAN_DRIVE="File" -e ALIYUNPAN_PAN_DIR="/" tickstep/aliyunpan-webdav
|
||||
|
||||
# ALIYUNPAN_REFRESH_TOKEN RefreshToken
|
||||
# ALIYUNPAN_AUTH_USER webdav登录用户名
|
||||
# ALIYUNPAN_AUTH_PASSWORD webdav登录密码
|
||||
# ALIYUNPAN_PAN_DRIVE 网盘类型,可选: File-文件 Album-相册
|
||||
# ALIYUNPAN_PAN_DIR 网盘文件夹的webdav服务根目录
|
||||
```
|
||||
|
||||
2. docker-compose运行
|
||||
docker-compose.yml 文件如下所示,为了方便说明增加了相关的注释,部署的时候可以去掉注释。
|
||||
```
|
||||
version: '3'
|
||||
services:
|
||||
webdav:
|
||||
image: tickstep/aliyunpan-webdav
|
||||
container_name: aliyunpan-webdav
|
||||
restart: always
|
||||
ports:
|
||||
- 23077:23077
|
||||
environment:
|
||||
- TZ=Asia/Shanghai
|
||||
# refresh token用于登录
|
||||
- ALIYUNPAN_REFRESH_TOKEN=b9123...13e66a1
|
||||
# webdav 登录用户名
|
||||
- ALIYUNPAN_AUTH_USER=admin
|
||||
# webdav 登录密码
|
||||
- ALIYUNPAN_AUTH_PASSWORD=admin
|
||||
# 指定网盘类型为文件,可选: File-文件 Album-相册
|
||||
- ALIYUNPAN_PAN_DRIVE=File
|
||||
# 指定网盘文件夹作为webdav服务根目录
|
||||
- ALIYUNPAN_PAN_DIR=/
|
||||
# 上传下载链接类型:1-默认 2-阿里ECS环境
|
||||
- ALIYUNPAN_TRANSFER_URL_TYPE=1
|
||||
# 上传数据块大小,单位为KB,默认为10240KB,建议范围1024KB~10240KB
|
||||
- ALIYUNPAN_BLOCK_SIZE=10240
|
||||
```
|
||||
|
||||
### HTTPS配置
|
||||
建议使用nginx进行https的配置。样例如下:
|
||||
```
|
||||
server {
|
||||
listen 443;
|
||||
server_name your.host.com;
|
||||
ssl on;
|
||||
root html;
|
||||
index index.html index.htm;
|
||||
ssl_certificate /path/to/your/file.pem;
|
||||
ssl_certificate_key /path/to/your/file.key;
|
||||
|
||||
ssl_session_timeout 5m;
|
||||
ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4;
|
||||
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
# webdav server
|
||||
location /{
|
||||
root html;
|
||||
proxy_pass http://127.0.0.1:23077;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header REMOTE-HOST $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_redirect off;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## JavaScript插件
|
||||
支持javascript插件,你可以按照自己的需要定制上传/下载中关键步骤的行为,最大程度满足自己的个性化需求。
|
||||
例如:
|
||||
@@ -1335,6 +1137,17 @@ function userTokenRefreshFinishCallback(context, params) {
|
||||
};
|
||||
try {
|
||||
if (params["result"] === "fail") {
|
||||
// 避免频繁发送
|
||||
var ONE_MINUTE = 60 * 1000;
|
||||
var lastSendEmailTime = PluginUtil.KV.getString("email_last_send_time");
|
||||
if (lastSendEmailTime != "") {
|
||||
if ((Date.now() - lastSendEmailTime) < (10 * ONE_MINUTE)) {
|
||||
console.log("距离上次发送邮件小于10分钟,先不发送了");
|
||||
return
|
||||
}
|
||||
}
|
||||
PluginUtil.KV.putString("email_last_send_time", Date.now());
|
||||
|
||||
var reqData = {
|
||||
"text": "Token刷新失败",
|
||||
"desp": params["message"]
|
||||
@@ -1349,6 +1162,33 @@ function userTokenRefreshFinishCallback(context, params) {
|
||||
}
|
||||
```
|
||||
|
||||
或者发送邮件通知
|
||||
```js
|
||||
function userTokenRefreshFinishCallback(context, params) {
|
||||
try {
|
||||
if (params["result"] === "fail") {
|
||||
// 避免频繁发送
|
||||
var ONE_MINUTE = 60 * 1000;
|
||||
var lastSendEmailTime = PluginUtil.KV.getString("email_last_send_time");
|
||||
if (lastSendEmailTime != "") {
|
||||
if ((Date.now() - lastSendEmailTime) < (10 * ONE_MINUTE)) {
|
||||
console.log("距离上次发送邮件小于10分钟,先不发送了");
|
||||
return
|
||||
}
|
||||
}
|
||||
PluginUtil.KV.putString("email_last_send_time", Date.now());
|
||||
|
||||
// 发送通知邮件,请确保你的发送邮箱具备smtp发送权限,没有的需要找邮箱提供商配置开启
|
||||
console.log("发送通知邮件");
|
||||
PluginUtil.Email.sendTextMail("smtp.qq.com:465", "123xxx@qq.com", "pwdxxxxxx", "12545xxx@qq.com", "Token刷新失败了", "Token过期了,骚年快去手动恢复吧");
|
||||
}
|
||||
} catch (e) {
|
||||
if (e !== "Error") {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 显示和修改程序配置项
|
||||
```
|
||||
@@ -1409,15 +1249,4 @@ Windows
|
||||
|
||||
### 第二步
|
||||
打开aliyunpan命令行程序,任何云盘命令都有类似如下日志输出
|
||||

|
||||
|
||||
## 3 解决 missing Location in call to Date 问题
|
||||
在使用webdav文件服务的过程中可能会出现该问题。
|
||||
原因是golang中的time.LoadLocation()依赖于 IANA Time Zone Database,一般linux系统都带了,但是有部分系统没有带有这个数据文件则出现该问题。
|
||||
解决方法:
|
||||
1. 下载文件 ./assets/binary/tzdata.zip 文件并保存到系统中,记下保存的文件路径
|
||||
2. 设置环境变量 ZONEINFO
|
||||
```
|
||||
export ZONEINFO=/path/to/tzdata.zip
|
||||
```
|
||||
然后启动aliyunpan服务即可。
|
||||

|
||||
@@ -1,28 +1,42 @@
|
||||
module github.com/tickstep/aliyunpan
|
||||
|
||||
go 1.16
|
||||
go 1.20
|
||||
|
||||
require (
|
||||
github.com/GeertJohan/go.incremental v1.0.0
|
||||
github.com/boltdb/bolt v1.3.1 // indirect
|
||||
github.com/deckarep/golang-set v1.8.0
|
||||
github.com/dop251/goja v0.0.0-20220408131256-ffe77e20c6f1
|
||||
github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e
|
||||
github.com/json-iterator/go v1.1.12
|
||||
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/oleiade/lane v0.0.0-20160817071224-3053869314bb
|
||||
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.1.2
|
||||
github.com/tickstep/bolt v1.3.3
|
||||
github.com/tickstep/library-go v0.0.8
|
||||
github.com/tickstep/aliyunpan-api v0.1.7
|
||||
github.com/tickstep/bolt v1.3.4
|
||||
github.com/tickstep/library-go v0.1.0
|
||||
github.com/urfave/cli v1.21.1-0.20190817182405-23c83030263f
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/boltdb/bolt v1.3.1 // indirect
|
||||
github.com/btcsuite/btcd v0.22.1 // indirect
|
||||
github.com/cpuguy83/go-md2man v1.0.10 // indirect
|
||||
github.com/denisbrodbeck/machineid v1.0.1 // indirect
|
||||
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 // indirect
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
|
||||
github.com/mattn/go-runewidth v0.0.9 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/russross/blackfriday v1.5.2 // indirect
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
)
|
||||
|
||||
//replace github.com/boltdb/bolt => github.com/tickstep/bolt v1.3.4
|
||||
//replace github.com/tickstep/bolt => /Users/tickstep/Documents/Workspace/go/projects/bolt
|
||||
//replace github.com/tickstep/library-go => /Users/tickstep/Documents/Workspace/go/projects/library-go
|
||||
//replace github.com/tickstep/aliyunpan-api => /Users/tickstep/Documents/Workspace/go/projects/aliyunpan-api
|
||||
|
||||
@@ -1,16 +1,34 @@
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg=
|
||||
github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0=
|
||||
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
|
||||
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
|
||||
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
|
||||
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
|
||||
github.com/btcsuite/btcd v0.22.1 h1:CnwP9LM/M9xuRrGSCGeMVs9iv09uMqwsVX7EeIpgV2c=
|
||||
github.com/btcsuite/btcd v0.22.1/go.mod h1:wqgTSL29+50LRkmOVknEdmt8ZojIzhuWvgu/iptuN7Y=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1 h1:q0rUy8C/TYNBQS1+CGKw68tLOFYSNEs0TFnxxnS9+4U=
|
||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc=
|
||||
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA=
|
||||
github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
|
||||
github.com/btcsuite/btcutil v1.0.3-0.20201208143702-a53e38424cce/go.mod h1:0DVlHczLPewLcPGEIeUEzfOJhqGPQ0mJJRDBtD307+o=
|
||||
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg=
|
||||
github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY=
|
||||
github.com/btcsuite/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I=
|
||||
github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||
github.com/btcsuite/snappy-go v1.0.0/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc=
|
||||
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY=
|
||||
github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs=
|
||||
github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF22jk=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4=
|
||||
github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo=
|
||||
github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218=
|
||||
github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
|
||||
github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
|
||||
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91 h1:Izz0+t1Z5nI16/II7vuEo/nHjodOg0p7+OiDpjX5t1E=
|
||||
@@ -20,16 +38,23 @@ github.com/dop251/goja v0.0.0-20220408131256-ffe77e20c6f1 h1:9/4Hyp+A98nqssD7qce
|
||||
github.com/dop251/goja v0.0.0-20220408131256-ffe77e20c6f1/go.mod h1:TQJQ+ZNyFVvUtUEtCZxBhfWiH7RJqR3EivNmvD6Waik=
|
||||
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
|
||||
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible h1:W1iEw64niKVGogNgBN3ePyLFfuisuzeidWPMPWmECqU=
|
||||
github.com/go-sourcemap/sourcemap v2.1.3+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e h1:OGunVjqY7y4U4laftpEHv+mvZBlr7UGimJXKEGQtg48=
|
||||
github.com/jordan-wright/email v0.0.0-20200602115436-fd8a7622303e/go.mod h1:Fy2gCFfZhay8jplf/Csj6cyH/oshQTkLQYZbKkcV+SY=
|
||||
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1 h1:PJPDf8OUfOK1bb/NeTKd4f1QXZItOX389VN3B6qC8ro=
|
||||
github.com/kardianos/osext v0.0.0-20170510131534-ae77be60afb1/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8=
|
||||
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
@@ -53,6 +78,10 @@ github.com/oleiade/lane v0.0.0-20160817071224-3053869314bb/go.mod h1:ym0w0flrmBt
|
||||
github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
|
||||
github.com/olekukonko/tablewriter v0.0.2-0.20190618033246-cc27d85e17ce h1:RLmZmfx/K62HKpbwPqtW3tg+V2GgugN/XNNx+uiMH/Y=
|
||||
github.com/olekukonko/tablewriter v0.0.2-0.20190618033246-cc27d85e17ce/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/peterh/liner v1.2.1 h1:O4BlKaq/LWu6VRWmol4ByWfzx6MfXc5Op5HETyIy5yg=
|
||||
github.com/peterh/liner v1.2.1/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
@@ -69,30 +98,33 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tickstep/aliyunpan-api v0.1.2 h1:QbrVUq6neuzmp1Zmfa8utphRhKQcKU4/seMs8RoNFK4=
|
||||
github.com/tickstep/aliyunpan-api v0.1.2/go.mod h1:jAlZgcXpg8n5nRSRrB9GxcEGr+DgwCzRKVi6d5ai2fI=
|
||||
github.com/tickstep/bolt v1.3.3 h1:3abb88T5JJocnBlYTJE3EqKvwWV6+D/JZD2Fsdt1QHI=
|
||||
github.com/tickstep/bolt v1.3.3/go.mod h1:Y24RwCywbOsKAyHiVcJ4K2QQfED+q67x7PEOH9OG7Q4=
|
||||
github.com/tickstep/library-go v0.0.8 h1:7cAgmM/PIkox4SS8R8m4UI86bQ7KRnm/KQYFloTJXR0=
|
||||
github.com/tickstep/library-go v0.0.8/go.mod h1:egoK/RvOJ3Qs2tHpkq374CWjhNjI91JSCCG1GrhDYSw=
|
||||
github.com/tickstep/aliyunpan-api v0.1.7 h1:YaKDT7AnYR7f0YM8zC4QE7sL6xljBFRz7BOjkIaGfNY=
|
||||
github.com/tickstep/aliyunpan-api v0.1.7/go.mod h1:SN+N5Vh3lLq3dVimlxKxviCl50gLGXZO96nHHc3qd2o=
|
||||
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.0 h1:lzz6wpYCeliSzmVISa7rp9bGB4r+uIa9/VUTtk87DFU=
|
||||
github.com/tickstep/library-go v0.1.0/go.mod h1:uAHeNOIpoywCzlaeLrWmmRSupn03m9kJVZKOEmuarmA=
|
||||
github.com/urfave/cli v1.21.1-0.20190817182405-23c83030263f h1:xKDKjIsL76VUyHcA0G4Qe1cIAUB/nrq6Pt8D411bd1g=
|
||||
github.com/urfave/cli v1.21.1-0.20190817182405-23c83030263f/go.mod h1:qXyCeJubPqsgeiLd3kvHOGHHSrQcNdjZ2ScXIcVZK/I=
|
||||
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc=
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
|
||||
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
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-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
@@ -102,6 +134,9 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
|
||||
+251
-3
@@ -4,7 +4,7 @@
|
||||
// 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
|
||||
// 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,
|
||||
@@ -21,10 +21,19 @@ import (
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"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/taskframework"
|
||||
"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/filepath"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -43,10 +52,10 @@ func CmdAlbum() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "album",
|
||||
Aliases: []string{"abm"},
|
||||
Usage: "相簿(Beta)",
|
||||
Usage: "个人相册(Beta)",
|
||||
UsageText: cmder.App().Name + " album",
|
||||
Category: "阿里云盘",
|
||||
Before: cmder.ReloadConfigFunc,
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
@@ -224,6 +233,70 @@ func CmdAlbum() cli.Command {
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
{
|
||||
Name: "download-file",
|
||||
Aliases: []string{"df"},
|
||||
Usage: "下载相簿中的所有文件到本地",
|
||||
UsageText: cmder.App().Name + " album download-file",
|
||||
Description: `
|
||||
下载相簿中的所有文件
|
||||
示例:
|
||||
|
||||
下载相簿 "我的相簿2022" 里面的所有文件
|
||||
aliyunpan album download-file 我的相簿2022
|
||||
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
subArgs := c.Args()
|
||||
if len(subArgs) == 0 {
|
||||
fmt.Println("请指定下载的相簿名称")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 处理saveTo
|
||||
var (
|
||||
saveTo string
|
||||
)
|
||||
if c.String("saveto") != "" {
|
||||
saveTo = filepath.Clean(c.String("saveto"))
|
||||
}
|
||||
|
||||
do := &DownloadOptions{
|
||||
IsPrintStatus: false,
|
||||
IsExecutedPermission: false,
|
||||
IsOverwrite: c.Bool("ow"),
|
||||
SaveTo: saveTo,
|
||||
Parallel: 0,
|
||||
Load: 0,
|
||||
MaxRetry: pandownload.DefaultDownloadMaxRetry,
|
||||
NoCheck: false,
|
||||
ShowProgress: !c.Bool("np"),
|
||||
DriveId: parseDriveId(c),
|
||||
ExcludeNames: []string{},
|
||||
}
|
||||
|
||||
RunAlbumDownloadFile(c.Args(), do)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "ow",
|
||||
Usage: "overwrite, 覆盖已存在的文件",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "saveto",
|
||||
Usage: "将下载的文件直接保存到指定的目录",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "np",
|
||||
Usage: "no progress 不展示下载进度条",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -503,3 +576,178 @@ func isFileMatchCondition(fileInfo *aliyunpan.FileEntity, filterOption AlbumFile
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
||||
if len(albumNames) == 0 {
|
||||
fmt.Printf("相簿名称不能为空\n")
|
||||
return
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
activeUser.PanClient().EnableCache()
|
||||
activeUser.PanClient().ClearCache()
|
||||
defer activeUser.PanClient().DisableCache()
|
||||
// pan token expired checker
|
||||
continueFlag := int32(0)
|
||||
atomic.StoreInt32(&continueFlag, 0)
|
||||
defer func() {
|
||||
atomic.StoreInt32(&continueFlag, 1)
|
||||
}()
|
||||
go func(flag *int32) {
|
||||
for atomic.LoadInt32(flag) == 0 {
|
||||
time.Sleep(time.Duration(1) * time.Minute)
|
||||
if RefreshTokenInNeed(activeUser, config.Config.DeviceName) {
|
||||
logger.Verboseln("update access token for download task")
|
||||
}
|
||||
}
|
||||
}(&continueFlag)
|
||||
|
||||
if options == nil {
|
||||
options = &DownloadOptions{}
|
||||
}
|
||||
|
||||
if options.MaxRetry < 0 {
|
||||
options.MaxRetry = pandownload.DefaultDownloadMaxRetry
|
||||
}
|
||||
options.IsExecutedPermission = false
|
||||
|
||||
// 设置下载配置
|
||||
cfg := &downloader.Config{
|
||||
Mode: transfer.RangeGenMode_BlockSize,
|
||||
CacheSize: config.Config.CacheSize,
|
||||
BlockSize: MaxDownloadRangeSize,
|
||||
MaxRate: config.Config.MaxDownloadRate,
|
||||
InstanceStateStorageFormat: downloader.InstanceStateStorageFormatJSON,
|
||||
ShowProgress: options.ShowProgress,
|
||||
UseInternalUrl: config.Config.TransferUrlType == 2,
|
||||
ExcludeNames: options.ExcludeNames,
|
||||
}
|
||||
if cfg.CacheSize == 0 {
|
||||
cfg.CacheSize = int(DownloadCacheSize)
|
||||
}
|
||||
|
||||
// 设置下载最大并发量
|
||||
if options.Parallel < 1 {
|
||||
options.Parallel = config.Config.MaxDownloadParallel
|
||||
if options.Parallel == 0 {
|
||||
options.Parallel = config.DefaultFileDownloadParallelNum
|
||||
}
|
||||
}
|
||||
if options.Parallel > config.MaxFileDownloadParallelNum {
|
||||
options.Parallel = config.MaxFileDownloadParallelNum
|
||||
}
|
||||
|
||||
// 保存文件的本地根文件夹
|
||||
originSaveRootPath := ""
|
||||
if options.SaveTo != "" {
|
||||
originSaveRootPath = options.SaveTo
|
||||
} else {
|
||||
// 使用默认的保存路径
|
||||
originSaveRootPath = GetActiveUser().GetSavePath("")
|
||||
}
|
||||
fi, err1 := os.Stat(originSaveRootPath)
|
||||
if err1 != nil && !os.IsExist(err1) {
|
||||
os.MkdirAll(originSaveRootPath, 0777) // 首先在本地创建目录
|
||||
} else {
|
||||
if !fi.IsDir() {
|
||||
fmt.Println("本地保存路径不是文件夹,请删除或者创建对应的文件夹:", originSaveRootPath)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\n[0] 当前文件下载最大并发量为: %d, 下载缓存为: %s\n\n", options.Parallel, converter.ConvertFileSize(int64(cfg.CacheSize), 2))
|
||||
|
||||
var (
|
||||
panClient = activeUser.PanClient()
|
||||
)
|
||||
cfg.MaxParallel = options.Parallel
|
||||
|
||||
var (
|
||||
executor = taskframework.TaskExecutor{
|
||||
IsFailedDeque: true, // 统计失败的列表
|
||||
}
|
||||
statistic = &pandownload.DownloadStatistic{}
|
||||
)
|
||||
// 配置执行器任务并发数,即同时下载文件并发数
|
||||
executor.SetParallel(cfg.MaxParallel)
|
||||
|
||||
// 全局速度统计
|
||||
globalSpeedsStat := &speeds.Speeds{}
|
||||
|
||||
// 处理队列
|
||||
for k := range albumNames {
|
||||
record := getAlbumFromName(activeUser, albumNames[k])
|
||||
if record == nil {
|
||||
continue
|
||||
}
|
||||
// 获取相簿下的所有文件
|
||||
fileList, er := activeUser.PanClient().AlbumListFileGetAll(&aliyunpan.AlbumListFileParam{
|
||||
AlbumId: record.AlbumId,
|
||||
})
|
||||
if er != nil {
|
||||
fmt.Printf("获取相簿文件出错,请稍后重试: %s\n", albumNames[k])
|
||||
continue
|
||||
}
|
||||
if fileList == nil || len(fileList) == 0 {
|
||||
fmt.Printf("相簿里面没有文件: %s\n", albumNames[k])
|
||||
continue
|
||||
}
|
||||
for _, f := range fileList {
|
||||
// 补全虚拟网盘路径,规则:/<相簿名称>/文件名称
|
||||
f.Path = "/" + albumNames[k] + "/" + f.FileName
|
||||
|
||||
// 生成下载项
|
||||
newCfg := *cfg
|
||||
unit := pandownload.DownloadTaskUnit{
|
||||
Cfg: &newCfg, // 复制一份新的cfg
|
||||
PanClient: panClient,
|
||||
VerbosePrinter: panCommandVerbose,
|
||||
PrintFormat: downloadPrintFormat(options.Load),
|
||||
ParentTaskExecutor: &executor,
|
||||
DownloadStatistic: statistic,
|
||||
IsPrintStatus: options.IsPrintStatus,
|
||||
IsExecutedPermission: options.IsExecutedPermission,
|
||||
IsOverwrite: options.IsOverwrite,
|
||||
NoCheck: options.NoCheck,
|
||||
FilePanPath: f.Path,
|
||||
DriveId: f.DriveId, // 必须使用文件的DriveId,因为一个相簿的文件会来自多个网盘(资源库/备份盘)
|
||||
GlobalSpeedsStat: globalSpeedsStat,
|
||||
FileRecorder: nil,
|
||||
}
|
||||
// 设置相簿文件信息
|
||||
unit.SetFileInfo(pandownload.AlbumFileSource, f)
|
||||
|
||||
// 设置储存的路径
|
||||
if options.SaveTo != "" {
|
||||
unit.OriginSaveRootPath = options.SaveTo
|
||||
unit.SavePath = filepath.Join(options.SaveTo, f.Path)
|
||||
} else {
|
||||
// 使用默认的保存路径
|
||||
unit.OriginSaveRootPath = GetActiveUser().GetSavePath("")
|
||||
unit.SavePath = GetActiveUser().GetSavePath(f.Path)
|
||||
}
|
||||
info := executor.Append(&unit, options.MaxRetry)
|
||||
fmt.Printf("[%s] 加入下载队列: %s\n", info.Id(), f.Path)
|
||||
}
|
||||
}
|
||||
|
||||
// 开始计时
|
||||
statistic.StartTimer()
|
||||
|
||||
// 开始执行
|
||||
executor.Execute()
|
||||
|
||||
fmt.Printf("\n下载结束, 时间: %s, 数据总量: %s\n", utils.ConvertTime(statistic.Elapsed()), converter.ConvertFileSize(statistic.TotalSize(), 2))
|
||||
|
||||
// 输出失败的文件列表
|
||||
failedList := executor.FailedDeque()
|
||||
if failedList.Size() != 0 {
|
||||
fmt.Printf("以下文件下载失败: \n")
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
for e := failedList.Shift(); e != nil; e = failedList.Shift() {
|
||||
item := e.(*taskframework.TaskInfoItem)
|
||||
tb.Append([]string{item.Info.Id(), item.Unit.(*pandownload.DownloadTaskUnit).FilePanPath})
|
||||
}
|
||||
tb.Render()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// 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
|
||||
// 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,
|
||||
@@ -43,8 +43,8 @@ func CmdCd() cli.Command {
|
||||
切换根目录:
|
||||
aliyunpan cd /
|
||||
`,
|
||||
Before: cmder.ReloadConfigFunc,
|
||||
After: cmder.SaveConfigFunc,
|
||||
Before: ReloadConfigFunc,
|
||||
After: SaveConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() == 0 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
@@ -73,7 +73,7 @@ func CmdPwd() cli.Command {
|
||||
Usage: "输出工作目录",
|
||||
UsageText: cmder.App().Name + " pwd",
|
||||
Category: "阿里云盘",
|
||||
Before: cmder.ReloadConfigFunc,
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
@@ -82,6 +82,8 @@ func CmdPwd() cli.Command {
|
||||
activeUser := config.Config.ActiveUser()
|
||||
if activeUser.IsFileDriveActive() {
|
||||
fmt.Println(activeUser.Workdir)
|
||||
} else if activeUser.IsResourceDriveActive() {
|
||||
fmt.Println(activeUser.ResourceWorkdir)
|
||||
} else if activeUser.IsAlbumDriveActive() {
|
||||
fmt.Println(activeUser.AlbumWorkdir)
|
||||
}
|
||||
@@ -125,6 +127,9 @@ func RunChangeDirectory(driveId, targetPath string) {
|
||||
if user.IsFileDriveActive() {
|
||||
user.Workdir = targetPathInfo.Path
|
||||
user.WorkdirFileEntity = *targetPathInfo
|
||||
} else if user.IsResourceDriveActive() {
|
||||
user.ResourceWorkdir = targetPathInfo.Path
|
||||
user.ResourceWorkdirFileEntity = *targetPathInfo
|
||||
} else if user.IsAlbumDriveActive() {
|
||||
user.AlbumWorkdir = targetPathInfo.Path
|
||||
user.AlbumWorkdirFileEntity = *targetPathInfo
|
||||
|
||||
@@ -97,7 +97,7 @@ func newRapidUploadItem(rapidUploadShareLink string) (*RapidUploadItem, error) {
|
||||
item.FileSha1 = strings.TrimSpace(parts[1])
|
||||
|
||||
// size
|
||||
if size,e := strconv.ParseInt(parts[2], 10, 64); e == nil{
|
||||
if size, e := strconv.ParseInt(parts[2], 10, 64); e == nil {
|
||||
item.FileSize = size
|
||||
} else {
|
||||
return nil, fmt.Errorf("文件大小错误: %s", rapidUploadShareLink)
|
||||
@@ -131,7 +131,6 @@ func (r *RapidUploadItem) createRapidUploadLink(hideRelativePath bool) string {
|
||||
p := r.FilePath
|
||||
p = strings.ReplaceAll(p, "\\", "/")
|
||||
|
||||
|
||||
fileName := path.Base(p)
|
||||
dirPath := path.Dir(p)
|
||||
|
||||
@@ -160,10 +159,10 @@ func CmdConfig() cli.Command {
|
||||
Usage: "显示和修改程序配置项",
|
||||
Description: "显示和修改程序配置项",
|
||||
Category: "配置",
|
||||
Before: cmder.ReloadConfigFunc,
|
||||
After: cmder.SaveConfigFunc,
|
||||
Before: ReloadConfigFunc,
|
||||
After: SaveConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
fmt.Printf("----\n运行 %s config set 可进行设置配置\n\n当前配置:\n", cmder.App().Name)
|
||||
fmt.Printf("----\n当前配置目录: %s\n运行 %s config set 可进行设置配置\n\n当前配置:\n", config.GetConfigDir(), cmder.App().Name)
|
||||
config.Config.PrintTable()
|
||||
return nil
|
||||
},
|
||||
@@ -227,6 +226,12 @@ func CmdConfig() cli.Command {
|
||||
if c.IsSet("local_addrs") {
|
||||
config.Config.SetLocalAddrs(c.String("local_addrs"))
|
||||
}
|
||||
if c.IsSet("file_record_config") {
|
||||
config.Config.SetFileRecorderConfig(c.String("file_record_config"))
|
||||
}
|
||||
if c.IsSet("device_id") {
|
||||
config.Config.SetDeviceId(c.String("device_id"))
|
||||
}
|
||||
|
||||
err := config.Config.Save()
|
||||
if err != nil {
|
||||
@@ -277,13 +282,20 @@ func CmdConfig() cli.Command {
|
||||
Name: "local_addrs",
|
||||
Usage: "设置本地网卡地址, 多个地址用逗号隔开",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "file_record_config",
|
||||
Usage: "设置是否开启上传、下载、同步文件的结果记录功能",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "device_id",
|
||||
Usage: "设置客户端ID,24位的字符串",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func CmdTool() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "tool",
|
||||
|
||||
@@ -20,6 +20,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/log"
|
||||
"github.com/tickstep/aliyunpan/internal/taskframework"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/aliyunpan/library/requester/transfer"
|
||||
@@ -31,6 +32,7 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -109,7 +111,7 @@ func CmdDownload() cli.Command {
|
||||
5)排除 myfile.txt 文件:-exn "^myfile.txt$"
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: cmder.ReloadConfigFunc,
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() == 0 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
@@ -142,7 +144,20 @@ func CmdDownload() cli.Command {
|
||||
ExcludeNames: c.StringSlice("exn"),
|
||||
}
|
||||
|
||||
// 获取下载文件锁,保证下载操作单实例
|
||||
//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{
|
||||
@@ -211,14 +226,19 @@ func RunDownload(paths []string, options *DownloadOptions) {
|
||||
activeUser.PanClient().ClearCache()
|
||||
defer activeUser.PanClient().DisableCache()
|
||||
// pan token expired checker
|
||||
go func() {
|
||||
for {
|
||||
continueFlag := int32(0)
|
||||
atomic.StoreInt32(&continueFlag, 0)
|
||||
defer func() {
|
||||
atomic.StoreInt32(&continueFlag, 1)
|
||||
}()
|
||||
go func(flag *int32) {
|
||||
for atomic.LoadInt32(flag) == 0 {
|
||||
time.Sleep(time.Duration(1) * time.Minute)
|
||||
if RefreshTokenInNeed(activeUser) {
|
||||
if RefreshTokenInNeed(activeUser, config.Config.DeviceName) {
|
||||
logger.Verboseln("update access token for download task")
|
||||
}
|
||||
}
|
||||
}()
|
||||
}(&continueFlag)
|
||||
|
||||
if options == nil {
|
||||
options = &DownloadOptions{}
|
||||
@@ -302,6 +322,9 @@ func RunDownload(paths []string, options *DownloadOptions) {
|
||||
// 全局速度统计
|
||||
globalSpeedsStat := &speeds.Speeds{}
|
||||
|
||||
// 下载记录器
|
||||
fileRecorder := log.NewFileRecorder(config.GetLogDir() + "/download_file_records.csv")
|
||||
|
||||
// 处理队列
|
||||
for k := range paths {
|
||||
// 使用通配符匹配
|
||||
@@ -339,6 +362,7 @@ func RunDownload(paths []string, options *DownloadOptions) {
|
||||
FilePanPath: f.Path,
|
||||
DriveId: options.DriveId,
|
||||
GlobalSpeedsStat: globalSpeedsStat,
|
||||
FileRecorder: fileRecorder,
|
||||
}
|
||||
|
||||
// 设置储存的路径
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// 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
|
||||
// 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,
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
"fmt"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/urfave/cli"
|
||||
@@ -28,9 +27,9 @@ import (
|
||||
func CmdDrive() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "drive",
|
||||
Usage: "切换网盘(文件网盘/相册网盘)",
|
||||
Usage: "切换网盘(备份盘/资源库/相册)",
|
||||
Description: `
|
||||
切换已登录的阿里云盘的云工作网盘(文件网盘/相册网盘)
|
||||
切换已登录账号的阿里云盘的工作网盘(备份盘/资源库/相册)
|
||||
如果运行该条命令没有提供参数, 程序将会列出所有的网盘列表, 供选择切换.
|
||||
|
||||
示例:
|
||||
@@ -38,8 +37,8 @@ func CmdDrive() cli.Command {
|
||||
aliyunpan drive <driveId>
|
||||
`,
|
||||
Category: "阿里云盘账号",
|
||||
Before: cmder.ReloadConfigFunc,
|
||||
After: cmder.SaveConfigFunc,
|
||||
Before: ReloadConfigFunc,
|
||||
After: SaveConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
inputData := c.Args().Get(0)
|
||||
targetDriveId := strings.TrimSpace(inputData)
|
||||
@@ -101,6 +100,11 @@ func RunSwitchDriveList(targetDriveId string) {
|
||||
config.Config.ActiveUser().Workdir = "/"
|
||||
config.Config.ActiveUser().WorkdirFileEntity = *aliyunpan.NewFileEntityForRootDir()
|
||||
}
|
||||
} else if activeUser.IsResourceDriveActive() {
|
||||
if activeUser.ResourceWorkdir == "" {
|
||||
config.Config.ActiveUser().ResourceWorkdir = "/"
|
||||
config.Config.ActiveUser().ResourceWorkdirFileEntity = *aliyunpan.NewFileEntityForRootDir()
|
||||
}
|
||||
} else if activeUser.IsAlbumDriveActive() {
|
||||
if activeUser.AlbumWorkdir == "" {
|
||||
config.Config.ActiveUser().AlbumWorkdir = "/"
|
||||
|
||||
@@ -28,7 +28,6 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
|
||||
func CmdExport() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "export",
|
||||
@@ -50,7 +49,7 @@ func CmdExport() cli.Command {
|
||||
aliyunpan export / /Users/tickstep/Downloads/export_files.txt
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: cmder.ReloadConfigFunc,
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() < 2 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
@@ -75,12 +74,11 @@ func CmdExport() cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func RunExportFiles(driveId string, overwrite bool, panPaths []string, saveLocalFilePath string) {
|
||||
activeUser := config.Config.ActiveUser()
|
||||
panClient := activeUser.PanClient()
|
||||
|
||||
lfi,_ := os.Stat(saveLocalFilePath)
|
||||
lfi, _ := os.Stat(saveLocalFilePath)
|
||||
realSaveFilePath := saveLocalFilePath
|
||||
if lfi != nil {
|
||||
if lfi.IsDir() {
|
||||
@@ -94,7 +92,7 @@ func RunExportFiles(driveId string, overwrite bool, panPaths []string, saveLocal
|
||||
} else {
|
||||
// create file
|
||||
localDir := path.Dir(saveLocalFilePath)
|
||||
dirFs,_ := os.Stat(localDir)
|
||||
dirFs, _ := os.Stat(localDir)
|
||||
if dirFs != nil {
|
||||
if !dirFs.IsDir() {
|
||||
fmt.Println("指定的保存文件路径不合法")
|
||||
@@ -117,7 +115,7 @@ func RunExportFiles(driveId string, overwrite bool, panPaths []string, saveLocal
|
||||
return
|
||||
}
|
||||
|
||||
for _,panPath := range panPaths {
|
||||
for _, panPath := range panPaths {
|
||||
panPath = activeUser.PathJoin(driveId, panPath)
|
||||
panClient.FilesDirectoriesRecurseList(driveId, panPath, func(depth int, _ string, fd *aliyunpan.FileEntity, apiError *apierror.ApiError) bool {
|
||||
if apiError != nil {
|
||||
|
||||
@@ -64,7 +64,7 @@ func CmdImport() cli.Command {
|
||||
aliyunpan import -saveto=/ /Users/tickstep/Downloads/export_files.txt
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: cmder.ReloadConfigFunc,
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() < 1 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
|
||||
@@ -49,7 +49,7 @@ func CmdLocateUrl() cli.Command {
|
||||
aliyunpan locate -saveto "/Volumes/Downloads/file_url.txt" /我的资源
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: cmder.ReloadConfigFunc,
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() == 0 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
|
||||
@@ -16,7 +16,6 @@ package command
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdliner"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/functions/panlogin"
|
||||
@@ -42,8 +41,8 @@ func CmdLogin() cli.Command {
|
||||
aliyunpan login -QrCode
|
||||
`,
|
||||
Category: "阿里云盘账号",
|
||||
Before: cmder.ReloadConfigFunc, // 每次进行登录动作的时候需要调用刷新配置
|
||||
After: cmder.SaveConfigFunc, // 登录完成需要调用保存配置
|
||||
Before: ReloadConfigFunc, // 每次进行登录动作的时候需要调用刷新配置
|
||||
After: SaveConfigFunc, // 登录完成需要调用保存配置
|
||||
Action: func(c *cli.Context) error {
|
||||
refreshTokenStr := ""
|
||||
if refreshTokenStr == "" {
|
||||
@@ -61,7 +60,7 @@ func CmdLogin() cli.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
cloudUser, err := config.SetupUserByCookie(&webToken)
|
||||
cloudUser, err := config.SetupUserByCookie(&webToken, config.Config.DeviceId, config.Config.DeviceName)
|
||||
if cloudUser == nil {
|
||||
fmt.Println("登录失败: ", err)
|
||||
return nil
|
||||
@@ -93,8 +92,8 @@ func CmdLogout() cli.Command {
|
||||
Usage: "退出阿里帐号",
|
||||
Description: "退出当前登录的帐号",
|
||||
Category: "阿里云盘账号",
|
||||
Before: cmder.ReloadConfigFunc,
|
||||
After: cmder.SaveConfigFunc,
|
||||
Before: ReloadConfigFunc,
|
||||
After: SaveConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.NumLogins() == 0 {
|
||||
fmt.Println("未设置任何帐号, 不能退出")
|
||||
@@ -117,6 +116,13 @@ func CmdLogout() cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
// 云端注销登录
|
||||
if _, e := activeUser.PanClient().DeviceLogout(); e != nil {
|
||||
fmt.Printf("登出设备失败,请稍后重试: %s\n", e.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
// 删除用户信息
|
||||
deletedUser, err := config.Config.DeleteUser(activeUser.UserId)
|
||||
if err != nil {
|
||||
fmt.Printf("退出用户 %s, 失败, 错误: %s\n", activeUser.Nickname, err)
|
||||
@@ -142,7 +148,7 @@ func RunLogin(useQrCodeLogin bool, refreshToken string) (tokenId, refreshTokenSt
|
||||
fmt.Println("二维码登录错误:", err)
|
||||
return "", "", aliyunpan.WebLoginToken{}, err
|
||||
}
|
||||
fmt.Printf("请在浏览器打开以下链接进行扫码登录,链接有效时间为5分钟\n%s\n\n", qrCodeUrlResult.TokenUrl)
|
||||
fmt.Printf("请在浏览器打开以下链接进行扫码登录,链接有效时间为5分钟\n%s\n\n", qrCodeUrlResult.TokenUrl+"&deviceId="+config.Config.DeviceId)
|
||||
|
||||
// handler waiting
|
||||
line := cmdliner.NewLiner()
|
||||
@@ -183,6 +189,6 @@ func RunLogin(useQrCodeLogin bool, refreshToken string) (tokenId, refreshTokenSt
|
||||
tokenId = qrCodeUrlResult.TokenId
|
||||
}
|
||||
|
||||
refreshTokenStr, webToken, error = cmder.DoLoginHelper(refreshToken)
|
||||
refreshTokenStr, webToken, error = DoLoginHelper(refreshToken)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import (
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/tickstep/library-go/text"
|
||||
"github.com/urfave/cli"
|
||||
"os"
|
||||
"strconv"
|
||||
@@ -69,7 +68,7 @@ func CmdLs() cli.Command {
|
||||
aliyunpan ll /我的资源
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: cmder.ReloadConfigFunc,
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
@@ -141,15 +140,6 @@ func RunLs(driveId, targetPath string, lsOptions *LsOptions,
|
||||
orderBy aliyunpan.FileOrderBy, orderDirection aliyunpan.FileOrderDirection) {
|
||||
activeUser := config.Config.ActiveUser()
|
||||
targetPath = activeUser.PathJoin(driveId, targetPath)
|
||||
if targetPath[len(targetPath)-1] == '/' {
|
||||
targetPath = text.Substr(targetPath, 0, len(targetPath)-1)
|
||||
}
|
||||
|
||||
//targetPathInfo, err := activeUser.PanClient().FileInfoByPath(driveId, targetPath)
|
||||
//if err != nil {
|
||||
// fmt.Println(err)
|
||||
// return
|
||||
//}
|
||||
|
||||
files, err := matchPathByShellPattern(driveId, targetPath)
|
||||
if err != nil {
|
||||
|
||||
@@ -20,7 +20,6 @@ import (
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/urfave/cli"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CmdMkdir() cli.Command {
|
||||
@@ -29,7 +28,7 @@ func CmdMkdir() cli.Command {
|
||||
Usage: "创建目录",
|
||||
UsageText: cmder.App().Name + " mkdir <目录>",
|
||||
Category: "阿里云盘",
|
||||
Before: cmder.ReloadConfigFunc,
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() == 0 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
@@ -44,8 +43,8 @@ func CmdMkdir() cli.Command {
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "familyId",
|
||||
Usage: "家庭云ID",
|
||||
Name: "driveId",
|
||||
Usage: "网盘ID",
|
||||
Value: "",
|
||||
},
|
||||
},
|
||||
@@ -55,11 +54,9 @@ func CmdMkdir() cli.Command {
|
||||
func RunMkdir(driveId, name string) {
|
||||
activeUser := GetActiveUser()
|
||||
fullpath := activeUser.PathJoin(driveId, name)
|
||||
pathSlice := strings.Split(fullpath, "/")
|
||||
rs := &aliyunpan.MkdirResult{}
|
||||
err := apierror.NewFailedApiError("")
|
||||
|
||||
rs, err = activeUser.PanClient().MkdirRecursive(driveId,"", "", 0, pathSlice)
|
||||
rs, err = activeUser.PanClient().Mkdir(driveId, "", fullpath)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("创建文件夹失败:" + err.Error())
|
||||
@@ -74,4 +71,4 @@ func RunMkdir(driveId, name string) {
|
||||
} else {
|
||||
fmt.Println("创建文件夹失败: ", fullpath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// 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
|
||||
// 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,
|
||||
@@ -16,7 +16,6 @@ package command
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/urfave/cli"
|
||||
@@ -29,7 +28,7 @@ func CmdMv() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "mv",
|
||||
Usage: "移动文件/目录",
|
||||
UsageText: `移动:
|
||||
UsageText: `
|
||||
aliyunpan mv <文件/目录1> <文件/目录2> <文件/目录3> ... <目标目录>`,
|
||||
Description: `
|
||||
注意: 移动多个文件和目录时, 请确保每一个文件和目录都存在, 否则移动操作会失败。支持通配符匹配移动文件,通配符当前只能匹配文件名,不能匹配文件路径。
|
||||
@@ -43,7 +42,7 @@ func CmdMv() cli.Command {
|
||||
aliyunpan mv /我的资源/*.png /我的图片
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: cmder.ReloadConfigFunc,
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() <= 1 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// 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
|
||||
// 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,
|
||||
@@ -15,7 +15,6 @@ package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/urfave/cli"
|
||||
@@ -34,7 +33,7 @@ func CmdQuota() cli.Command {
|
||||
Usage: "获取当前帐号空间配额",
|
||||
Description: "获取网盘的总储存空间, 和已使用的储存空间",
|
||||
Category: "阿里云盘账号",
|
||||
Before: cmder.ReloadConfigFunc,
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
@@ -42,7 +41,7 @@ func CmdQuota() cli.Command {
|
||||
}
|
||||
q, err := RunGetQuotaInfo()
|
||||
if err == nil {
|
||||
fmt.Printf("账号: %s, uid: %s, 个人空间总额: %s, 个人空间已使用: %s, 比率: %f%%\n",
|
||||
fmt.Printf("账号: %s, uid: %s, 个人空间总额: %s, 个人空间已使用: %s, 比率: %.2f%%\n",
|
||||
config.Config.ActiveUser().Nickname, config.Config.ActiveUser().UserId,
|
||||
converter.ConvertFileSize(q.Quota, 2), converter.ConvertFileSize(q.UsedSize, 2),
|
||||
100*float64(q.UsedSize)/float64(q.Quota))
|
||||
@@ -59,6 +58,6 @@ func RunGetQuotaInfo() (quotaInfo *QuotaInfo, error error) {
|
||||
}
|
||||
return &QuotaInfo{
|
||||
UsedSize: int64(user.UsedSize),
|
||||
Quota: int64(user.TotalSize),
|
||||
Quota: int64(user.TotalSize),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ func CmdRecycle() cli.Command {
|
||||
aliyunpan recycle delete -all
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: cmder.ReloadConfigFunc,
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NumFlags() <= 0 || c.NArg() <= 0 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
|
||||
+178
-9
@@ -15,14 +15,42 @@ package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apiutil"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/urfave/cli"
|
||||
"path"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
fileItem struct {
|
||||
file *aliyunpan.FileEntity
|
||||
newFileName string
|
||||
}
|
||||
fileArray []*fileItem
|
||||
)
|
||||
|
||||
func newFileItem(file *aliyunpan.FileEntity) *fileItem {
|
||||
return &fileItem{
|
||||
file: file,
|
||||
newFileName: "",
|
||||
}
|
||||
}
|
||||
|
||||
func (x fileArray) Len() int {
|
||||
return len(x)
|
||||
}
|
||||
func (x fileArray) Less(i, j int) bool {
|
||||
return x[i].file.FileName < x[j].file.FileName
|
||||
}
|
||||
func (x fileArray) Swap(i, j int) {
|
||||
x[i], x[j] = x[j], x[i]
|
||||
}
|
||||
|
||||
func CmdRename() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "rename",
|
||||
@@ -31,18 +59,32 @@ func CmdRename() cli.Command {
|
||||
aliyunpan rename <旧文件/目录名> <新文件/目录名>`,
|
||||
Description: `
|
||||
示例:
|
||||
1. 将文件 1.mp4 重命名为 2.mp4
|
||||
aliyunpan rename 1.mp4 2.mp4
|
||||
|
||||
将文件 1.mp4 重命名为 2.mp4
|
||||
aliyunpan rename 1.mp4 2.mp4
|
||||
2. 将文件 /test/1.mp4 重命名为 /test/2.mp4
|
||||
要求必须是同一个文件目录内
|
||||
aliyunpan rename /test/1.mp4 /test/2.mp4
|
||||
|
||||
将文件 /test/1.mp4 重命名为 /test/2.mp4
|
||||
要求必须是同一个文件目录内
|
||||
aliyunpan rename /test/1.mp4 /test/2.mp4
|
||||
批量重命名,规则:rename [expression] [replacement] [file]
|
||||
其中,
|
||||
expression - 表达式,即文件名中需要去掉的部分,如果为*代表旧的文件名全部去掉
|
||||
replacement - 需要替换的新文件名,如果包含 # 号会按顺序替换成数字编号
|
||||
file - 文件匹配模式,指定需要查找的文件匹配字符串,支持通配符,例如:*.mp4 即当前目录下所有的mp4文件
|
||||
|
||||
3. 批量重命名,将当前目录下所有.mp4文件中包含的字符串 "我的视频" 全部改成 "视频"
|
||||
aliyunpan rename 我的视频 视频 *.mp4
|
||||
|
||||
4. 批量重命名,将当前目录下所有.mp4文件全部进行 "视频+编号.mp4" 的重命名操作,旧的名称全部去掉,即:视频001.mp4, 视频002.mp4, 视频003.mp4...
|
||||
aliyunpan rename * 视频###.mp4 *.mp4
|
||||
|
||||
5. 批量重命名,将当前目录下所有.mp4文件全部进行 "视频+编号.mp4" 的重命名操作,旧的名称全部去掉,直接重命名无需人工确认操作
|
||||
aliyunpan rename -y * 视频###.mp4 *.mp4
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: cmder.ReloadConfigFunc,
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() != 2 {
|
||||
if c.NArg() != 2 && c.NArg() != 3 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
@@ -50,7 +92,12 @@ func CmdRename() cli.Command {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
RunRename(parseDriveId(c), c.Args().Get(0), c.Args().Get(1))
|
||||
if c.NArg() == 2 {
|
||||
RunRename(parseDriveId(c), c.Args().Get(0), c.Args().Get(1))
|
||||
} else if c.NArg() == 3 {
|
||||
// 批量重命名
|
||||
RunRenameBatch(c.Bool("y"), parseDriveId(c), c.Args().Get(0), c.Args().Get(1), c.Args().Get(2))
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
@@ -59,6 +106,10 @@ func CmdRename() cli.Command {
|
||||
Usage: "网盘ID",
|
||||
Value: "",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "y",
|
||||
Usage: "跳过人工确认,对批量操作有效",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -104,3 +155,121 @@ func RunRename(driveId string, oldName string, newName string) {
|
||||
fmt.Printf("重命名文件成功:%s -> %s\n", path.Base(oldName), path.Base(newName))
|
||||
activeUser.DeleteOneCache(path.Dir(newName))
|
||||
}
|
||||
|
||||
// RunRenameBatch 批量重命名文件
|
||||
func RunRenameBatch(skipConfirm bool, driveId string, expression, replacement, filePattern string) {
|
||||
if len(expression) == 0 {
|
||||
fmt.Println("旧文件名不能为空")
|
||||
return
|
||||
}
|
||||
if !apiutil.CheckFileNameValid(replacement) {
|
||||
fmt.Println("新文件名不能包含特殊字符:" + apiutil.FileNameSpecialChars)
|
||||
return
|
||||
}
|
||||
|
||||
if len(filePattern) == 0 {
|
||||
fmt.Println("文件匹配模式不能为空")
|
||||
return
|
||||
}
|
||||
if strings.ContainsAny(filePattern, "/") {
|
||||
fmt.Println("文件匹配模式不能包含路径分隔符")
|
||||
return
|
||||
}
|
||||
if strings.ContainsAny(filePattern, "\\") {
|
||||
fmt.Println("文件匹配模式不能包含路径分隔符")
|
||||
return
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
absolutePath := path.Clean(activeUser.PathJoin(driveId, filePattern))
|
||||
fileList, err1 := matchPathByShellPattern(driveId, absolutePath)
|
||||
if err1 != nil {
|
||||
fmt.Println("查询文件出错:" + err1.Error())
|
||||
return
|
||||
}
|
||||
if fileList == nil || len(fileList) == 0 {
|
||||
fmt.Println("没有找到符合的文件")
|
||||
return
|
||||
}
|
||||
|
||||
// 文件列表按照名字排序
|
||||
files := fileArray{}
|
||||
for _, f := range fileList {
|
||||
files = append(files, newFileItem(f))
|
||||
}
|
||||
if len(files) > 1 {
|
||||
sort.Sort(files)
|
||||
}
|
||||
|
||||
// 依次处理
|
||||
var index = 1
|
||||
for _, file := range files {
|
||||
replacementStr := replaceNumStr(replacement, index)
|
||||
index += 1
|
||||
if expression == "*" {
|
||||
// 旧的名称全部替换
|
||||
file.newFileName = replacementStr
|
||||
} else {
|
||||
// 替换指定的名称片段
|
||||
file.newFileName = strings.ReplaceAll(file.file.FileName, expression, replacementStr)
|
||||
}
|
||||
}
|
||||
|
||||
// 确认
|
||||
if !skipConfirm {
|
||||
fmt.Printf("以下文件将进行对应的重命名\n\n")
|
||||
idx := 1
|
||||
for _, file := range files {
|
||||
fmt.Printf("%d) %s -> %s\n", idx, file.file.FileName, file.newFileName)
|
||||
idx += 1
|
||||
}
|
||||
fmt.Printf("\n是否进行批量重命名,该操作不可逆(y/n): ")
|
||||
confirm := ""
|
||||
_, err := fmt.Scanln(&confirm)
|
||||
if err != nil || (confirm != "y" && confirm != "Y") {
|
||||
fmt.Println("用户取消了操作")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 重命名
|
||||
for _, file := range files {
|
||||
b, e := activeUser.PanClient().FileRename(driveId, file.file.FileId, file.newFileName)
|
||||
if e != nil {
|
||||
fmt.Println(e.Err)
|
||||
return
|
||||
}
|
||||
if !b {
|
||||
fmt.Println("重命名文件失败")
|
||||
return
|
||||
}
|
||||
fmt.Printf("重命名文件成功:%s -> %s\n", file.file.FileName, file.newFileName)
|
||||
activeUser.DeleteOneCache(path.Dir(file.file.Path))
|
||||
}
|
||||
}
|
||||
|
||||
// replaceNumStr 将#替换成数字编号
|
||||
func replaceNumStr(name string, num int) string {
|
||||
pattern, _ := regexp.Compile("[#]+")
|
||||
r := pattern.FindAllStringSubmatchIndex(name, -1)
|
||||
if len(r) == 0 {
|
||||
return name
|
||||
}
|
||||
newName := name
|
||||
c := r[0]
|
||||
return replaceNumStr(newName[:c[0]]+generateNumStr(num, c[1]-c[0])+newName[c[1]:], num)
|
||||
}
|
||||
func generateNumStr(num, count int) string {
|
||||
s := strconv.Itoa(num)
|
||||
return generateZeroStr(count-len(s)) + s
|
||||
}
|
||||
func generateZeroStr(count int) string {
|
||||
if count <= 0 {
|
||||
return ""
|
||||
}
|
||||
s := ""
|
||||
for i := 0; i < count; i++ {
|
||||
s += "0"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"sort"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRenameSort(t *testing.T) {
|
||||
files := fileArray{}
|
||||
files = append(files, newFileItem(&aliyunpan.FileEntity{FileName: "0我的文件01.txt"}))
|
||||
files = append(files, newFileItem(&aliyunpan.FileEntity{FileName: "4我的文件03.txt"}))
|
||||
files = append(files, newFileItem(&aliyunpan.FileEntity{FileName: "3我的文件02.txt"}))
|
||||
files = append(files, newFileItem(&aliyunpan.FileEntity{FileName: "1我的文件00.txt"}))
|
||||
|
||||
sort.Sort(files)
|
||||
|
||||
for _, f := range files {
|
||||
fmt.Println(f.file.FileName)
|
||||
}
|
||||
}
|
||||
func TestRenameNum0(t *testing.T) {
|
||||
fmt.Println(replaceNumStr("我的文件###.txt", 2))
|
||||
}
|
||||
func TestRenameNum1(t *testing.T) {
|
||||
fmt.Println(replaceNumStr("我的##文件###.txt", 123))
|
||||
}
|
||||
func TestRenameNum2(t *testing.T) {
|
||||
fmt.Println(replaceNumStr("我的文件###.txt", 1233))
|
||||
}
|
||||
func TestRenameNum3(t *testing.T) {
|
||||
fmt.Println(replaceNumStr("我的文件[###].txt", 1233))
|
||||
}
|
||||
func TestRenameNum4(t *testing.T) {
|
||||
fmt.Println(replaceNumStr("我的文件.txt", 1233))
|
||||
}
|
||||
|
||||
func TestRenameNum5(t *testing.T) {
|
||||
fmt.Println(replaceNumStr("", 1233))
|
||||
}
|
||||
@@ -49,7 +49,7 @@ func CmdRm() cli.Command {
|
||||
aliyunpan rm /我的资源/*.zip
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: cmder.ReloadConfigFunc,
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() == 0 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
|
||||
+68
-34
@@ -4,7 +4,7 @@
|
||||
// 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
|
||||
// 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,
|
||||
@@ -36,7 +36,7 @@ func CmdShare() cli.Command {
|
||||
Usage: "分享文件/目录",
|
||||
UsageText: cmder.App().Name + " share",
|
||||
Category: "阿里云盘",
|
||||
Before: cmder.ReloadConfigFunc,
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
@@ -61,6 +61,9 @@ func CmdShare() cli.Command {
|
||||
|
||||
创建文件 1.mp4 的分享链接,并指定有效期为1天
|
||||
aliyunpan share set -time 1 1.mp4
|
||||
|
||||
创建文件 1.mp4 的快传链接
|
||||
aliyunpan share set -mode 3 1.mp4
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() < 1 {
|
||||
@@ -90,10 +93,18 @@ func CmdShare() cli.Command {
|
||||
sharePwd = c.String("sharePwd")
|
||||
}
|
||||
|
||||
modeFlag := "1"
|
||||
modeFlag := "3"
|
||||
if c.IsSet("mode") {
|
||||
modeFlag = c.String("mode")
|
||||
}
|
||||
if modeFlag == "1" || modeFlag == "2" {
|
||||
if config.Config.ActiveUser().ActiveDriveId != config.Config.ActiveUser().DriveList.GetResourceDriveId() {
|
||||
// 只有资源库才支持私有、公开分享
|
||||
fmt.Println("只有资源库才支持分享链接,其他请使用快传链接")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if modeFlag == "1" {
|
||||
if sharePwd == "" {
|
||||
sharePwd = RandomStr(4)
|
||||
@@ -101,7 +112,7 @@ func CmdShare() cli.Command {
|
||||
} else {
|
||||
sharePwd = ""
|
||||
}
|
||||
RunShareSet(parseDriveId(c), c.Args(), et, sharePwd)
|
||||
RunShareSet(modeFlag, parseDriveId(c), c.Args(), et, sharePwd)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
@@ -117,8 +128,8 @@ func CmdShare() cli.Command {
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "mode",
|
||||
Usage: "有效期,1-私密分享,2-公开分享",
|
||||
Value: "1",
|
||||
Usage: "模式,1-私密分享,2-公开分享,3-快传",
|
||||
Value: "3",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "sharePwd",
|
||||
@@ -238,7 +249,7 @@ func CmdShare() cli.Command {
|
||||
}
|
||||
|
||||
// RunShareSet 执行分享
|
||||
func RunShareSet(driveId string, paths []string, expiredTime string, sharePwd string) {
|
||||
func RunShareSet(modeFlag, driveId string, paths []string, expiredTime string, sharePwd string) {
|
||||
if len(paths) <= 0 {
|
||||
fmt.Println("请指定文件路径")
|
||||
return
|
||||
@@ -273,29 +284,47 @@ func RunShareSet(driveId string, paths []string, expiredTime string, sharePwd st
|
||||
return
|
||||
}
|
||||
|
||||
r, err1 := panClient.ShareLinkCreate(aliyunpan.ShareCreateParam{
|
||||
DriveId: driveId,
|
||||
SharePwd: sharePwd,
|
||||
Expiration: expiredTime,
|
||||
FileIdList: fidList,
|
||||
})
|
||||
|
||||
if err1 != nil || r == nil {
|
||||
if err1.Code == apierror.ApiCodeFileShareNotAllowed {
|
||||
fmt.Printf("创建分享链接失败: 该文件类型不允许分享\n")
|
||||
} else {
|
||||
fmt.Printf("创建分享链接失败: %s\n", err1)
|
||||
if modeFlag == "3" {
|
||||
// 快传
|
||||
r, err1 := panClient.FastShareLinkCreate(aliyunpan.FastShareCreateParam{
|
||||
DriveId: driveId,
|
||||
FileIdList: fidList,
|
||||
})
|
||||
if err1 != nil || r == nil {
|
||||
if err1.Code == apierror.ApiCodeFileShareNotAllowed {
|
||||
fmt.Printf("创建快传链接失败: 该文件类型不允许分享\n")
|
||||
} else {
|
||||
fmt.Printf("创建快传链接失败: %s\n", err1)
|
||||
}
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("创建分享链接成功\n")
|
||||
if len(sharePwd) > 0 {
|
||||
fmt.Printf("链接:%s 提取码:%s\n", r.ShareUrl, r.SharePwd)
|
||||
} else {
|
||||
fmt.Printf("创建快传链接成功\n")
|
||||
fmt.Printf("链接:%s\n", r.ShareUrl)
|
||||
}
|
||||
} else {
|
||||
// 分享
|
||||
r, err1 := panClient.ShareLinkCreate(aliyunpan.ShareCreateParam{
|
||||
DriveId: driveId,
|
||||
SharePwd: sharePwd,
|
||||
Expiration: expiredTime,
|
||||
FileIdList: fidList,
|
||||
})
|
||||
if err1 != nil || r == nil {
|
||||
if err1.Code == apierror.ApiCodeFileShareNotAllowed {
|
||||
fmt.Printf("创建分享链接失败: 该文件类型不允许分享\n")
|
||||
} else {
|
||||
fmt.Printf("创建分享链接失败: %s\n", err1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("创建分享链接成功\n")
|
||||
if len(sharePwd) > 0 {
|
||||
fmt.Printf("链接:%s 提取码:%s\n", r.ShareUrl, r.SharePwd)
|
||||
} else {
|
||||
fmt.Printf("链接:%s\n", r.ShareUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RunShareList 执行列出分享列表
|
||||
@@ -316,17 +345,22 @@ func RunShareList() {
|
||||
et = record.Expiration
|
||||
}
|
||||
status := "有效"
|
||||
if record.FirstFile == nil {
|
||||
status = "已删除"
|
||||
} else {
|
||||
cz := time.FixedZone("CST", 8*3600)
|
||||
if len(record.Expiration) > 0 {
|
||||
expiredTime, _ := time.ParseInLocation("2006-01-02 15:04:05", record.Expiration, cz)
|
||||
if expiredTime.Unix() < now.Unix() {
|
||||
status = "已过期"
|
||||
if record.Status == "enabled" {
|
||||
if record.FirstFile == nil {
|
||||
status = "已删除"
|
||||
} else {
|
||||
cz := time.FixedZone("CST", 8*3600)
|
||||
if len(record.Expiration) > 0 {
|
||||
expiredTime, _ := time.ParseInLocation("2006-01-02 15:04:05", record.Expiration, cz)
|
||||
if expiredTime.Unix() < now.Unix() {
|
||||
status = "已过期"
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if record.Status == "forbidden" {
|
||||
status = "违规"
|
||||
}
|
||||
|
||||
tb.Append([]string{strconv.Itoa(k + 1), record.ShareId, record.ShareUrl, record.SharePwd,
|
||||
record.ShareName,
|
||||
//record.FileIdList[0],
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/log"
|
||||
"github.com/tickstep/aliyunpan/internal/syncdrive"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
@@ -26,6 +27,7 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -50,7 +52,7 @@ func CmdSync() cli.Command {
|
||||
aliyunpan sync start -h
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: cmder.ReloadConfigFunc,
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
@@ -217,7 +219,20 @@ priority - 优先级,只对双向同步备份模式有效。选项支持三种
|
||||
task.UserId = activeUser.UserId
|
||||
}
|
||||
|
||||
// 获取同步文件锁,保证同步操作单实例
|
||||
//locker := filelocker.NewFileLocker(config.GetLockerDir() + "/aliyunpan-sync")
|
||||
//if e := filelocker.LockFile(locker, 0755, true, 5*time.Second); e != nil {
|
||||
// logger.Verboseln(e)
|
||||
// fmt.Println("本应用其他实例正在执行同步,请先停止或者等待其完成")
|
||||
// return nil
|
||||
//}
|
||||
|
||||
RunSync(task, dp, up, downloadBlockSize, uploadBlockSize, syncOpt, c.Int("ldt"), step)
|
||||
|
||||
// 释放文件锁
|
||||
//if locker != nil {
|
||||
// filelocker.UnlockFile(locker)
|
||||
//}
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
@@ -256,7 +271,7 @@ priority - 优先级,只对双向同步备份模式有效。选项支持三种
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "ubs",
|
||||
Usage: "upload block size,上传分片大小,单位KB。推荐值:1024 ~ 10240",
|
||||
Usage: "upload block size,上传分片大小,单位KB。推荐值:1024 ~ 10240。当上传极大单文件时候请适当调高该值",
|
||||
Value: 10240,
|
||||
},
|
||||
cli.StringFlag{
|
||||
@@ -290,15 +305,20 @@ func RunSync(defaultTask *syncdrive.SyncTask, fileDownloadParallel, fileUploadPa
|
||||
panClient.DisableCache()
|
||||
|
||||
// pan token expired checker
|
||||
go func() {
|
||||
for {
|
||||
continueFlag := int32(0)
|
||||
atomic.StoreInt32(&continueFlag, 0)
|
||||
defer func() {
|
||||
atomic.StoreInt32(&continueFlag, 1)
|
||||
}()
|
||||
go func(flag *int32) {
|
||||
for atomic.LoadInt32(flag) == 0 {
|
||||
time.Sleep(time.Duration(1) * time.Minute)
|
||||
if RefreshTokenInNeed(activeUser) {
|
||||
if RefreshTokenInNeed(activeUser, config.Config.DeviceName) {
|
||||
logger.Verboseln("update access token for sync task")
|
||||
panClient.UpdateToken(activeUser.WebToken)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}(&continueFlag)
|
||||
|
||||
syncFolderRootPath := config.GetSyncDriveDir()
|
||||
if b, e := utils.PathExists(syncFolderRootPath); e == nil {
|
||||
@@ -318,6 +338,10 @@ func RunSync(defaultTask *syncdrive.SyncTask, fileDownloadParallel, fileUploadPa
|
||||
if useInternalUrl {
|
||||
typeUrlStr = "阿里ECS内部链接"
|
||||
}
|
||||
|
||||
// 文件同步记录器
|
||||
fileRecorder := log.NewFileRecorder(config.GetLogDir() + "/sync_file_records.csv")
|
||||
|
||||
option := syncdrive.SyncOption{
|
||||
FileDownloadParallel: fileDownloadParallel,
|
||||
FileUploadParallel: fileUploadParallel,
|
||||
@@ -328,6 +352,7 @@ func RunSync(defaultTask *syncdrive.SyncTask, fileDownloadParallel, fileUploadPa
|
||||
MaxUploadRate: maxUploadRate,
|
||||
SyncPriority: flag,
|
||||
LocalFileModifiedCheckIntervalSec: localDelayTime,
|
||||
FileRecorder: fileRecorder,
|
||||
}
|
||||
syncMgr := syncdrive.NewSyncTaskManager(activeUser, activeUser.DriveList.GetFileDriveId(), panClient, syncFolderRootPath, option)
|
||||
syncConfigFile := syncMgr.ConfigFilePath()
|
||||
|
||||
@@ -30,7 +30,7 @@ func CmdToken() cli.Command {
|
||||
Usage: "Token相关操作",
|
||||
UsageText: cmder.App().Name + " token",
|
||||
Category: "阿里云盘账号",
|
||||
Before: cmder.ReloadConfigFunc,
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
@@ -76,7 +76,7 @@ func CmdToken() cli.Command {
|
||||
|
||||
// RunTokenUpdate 执行Token更新
|
||||
func RunTokenUpdate(modeFlag string) {
|
||||
cmder.ReloadConfigFunc(nil)
|
||||
ReloadConfigFunc(nil)
|
||||
|
||||
// 获取当前插件
|
||||
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
|
||||
@@ -125,5 +125,5 @@ func RunTokenUpdate(modeFlag string) {
|
||||
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er.Error())
|
||||
}
|
||||
}
|
||||
cmder.SaveConfigFunc(nil)
|
||||
SaveConfigFunc(nil)
|
||||
}
|
||||
|
||||
+77
-12
@@ -32,15 +32,33 @@ func CmdTree() cli.Command {
|
||||
|
||||
列出 /我的资源 内的文件和目录的树形图,并且显示文件对应的完整绝对路径
|
||||
aliyunpan tree -fp /我的资源
|
||||
|
||||
列出 /我的资源 内的文件和目录的树形图,并且显示文件对应的文件大小
|
||||
aliyunpan tree -fs /我的资源
|
||||
|
||||
列出 /我的资源 内的文件和目录的树形图,过滤大于等于 10kb 并且小于等于 10mb 的文件,同时显示文件对应的文件大小
|
||||
aliyunpan tree -fs -minSize=1kb -maxSize=10mb /我的资源
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: cmder.ReloadConfigFunc,
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
RunTree(parseDriveId(c), c.Args().Get(0), c.Bool("fp"))
|
||||
minSize := int64(0)
|
||||
if c.IsSet("minSize") {
|
||||
if s, e := converter.ParseFileSizeStr(c.String("minSize")); e == nil {
|
||||
minSize = s
|
||||
}
|
||||
}
|
||||
maxSize := int64(0)
|
||||
if c.IsSet("maxSize") {
|
||||
if s, e := converter.ParseFileSizeStr(c.String("maxSize")); e == nil {
|
||||
maxSize = s
|
||||
}
|
||||
}
|
||||
RunTree(parseDriveId(c), c.Args().Get(0), c.Bool("fp"), c.Bool("fs"), minSize, maxSize)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
@@ -51,7 +69,19 @@ func CmdTree() cli.Command {
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "fp",
|
||||
Usage: "full path, 树形图是否显示文件完整路径",
|
||||
Usage: "full path, 树形图显示文件的完整路径",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "fs",
|
||||
Usage: "file size, 树形图显示文件的文件大小",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "minSize",
|
||||
Usage: "min size, 过滤大于等于指定大小的文件,例如:100mb",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "maxSize",
|
||||
Usage: "max size, 过滤小于等于指定大小的文件,例如:1gb",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -69,9 +99,16 @@ type (
|
||||
CountOfFile int64
|
||||
SizeOfFile int64
|
||||
}
|
||||
|
||||
treeConfig struct {
|
||||
showFullPath bool
|
||||
showFileSize bool
|
||||
minFileSize int64
|
||||
maxFileSize int64
|
||||
}
|
||||
)
|
||||
|
||||
func getTree(driveId, pathStr string, depth int, statistic *treeStatistic, showFullPath bool) {
|
||||
func getTree(driveId, pathStr string, depth int, statistic *treeStatistic, setting *treeConfig) {
|
||||
activeUser := config.Config.ActiveUser()
|
||||
pathStr = activeUser.PathJoin(driveId, pathStr)
|
||||
pathStr = path.Clean(pathStr)
|
||||
@@ -109,7 +146,7 @@ func getTree(driveId, pathStr string, depth int, statistic *treeStatistic, showF
|
||||
fileListParam.OrderBy = aliyunpan.FileOrderByName
|
||||
fileListParam.OrderDirection = aliyunpan.FileOrderDirectionAsc
|
||||
if targetPathInfo.IsFolder() {
|
||||
fileResult, err := activeUser.PanClient().FileListGetAll(fileListParam, 100)
|
||||
fileResult, err := activeUser.PanClient().FileListGetAll(fileListParam, 500)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
@@ -127,14 +164,27 @@ func getTree(driveId, pathStr string, depth int, statistic *treeStatistic, showF
|
||||
for i, file := range fileList {
|
||||
if file.IsFolder() {
|
||||
statistic.CountOfDir += 1
|
||||
if showFullPath {
|
||||
if setting.showFullPath {
|
||||
fmt.Printf("%v%v %v/ -> %s\n", indentPrefixStr, pathPrefix, file.FileName, targetPathInfo.Path+"/"+file.FileName)
|
||||
} else {
|
||||
fmt.Printf("%v%v %v/\n", indentPrefixStr, pathPrefix, file.FileName)
|
||||
}
|
||||
getTree(driveId, targetPathInfo.Path+"/"+file.Path, depth+1, statistic, showFullPath)
|
||||
getTree(driveId, targetPathInfo.Path+"/"+file.Path, depth+1, statistic, setting)
|
||||
continue
|
||||
}
|
||||
|
||||
// filter file size
|
||||
if setting.minFileSize > 0 {
|
||||
if file.FileSize < setting.minFileSize {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if setting.maxFileSize > 0 {
|
||||
if file.FileSize > setting.maxFileSize {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
statistic.CountOfFile += 1
|
||||
statistic.SizeOfFile += file.FileSize
|
||||
|
||||
@@ -142,10 +192,19 @@ func getTree(driveId, pathStr string, depth int, statistic *treeStatistic, showF
|
||||
prefix = lastFilePrefix
|
||||
}
|
||||
|
||||
if showFullPath {
|
||||
fmt.Printf("%v%v %v -> %s\n", indentPrefixStr, prefix, file.FileName, targetPathInfo.Path+"/"+file.FileName)
|
||||
// 文件大小
|
||||
fileName := &strings.Builder{}
|
||||
if setting.showFileSize {
|
||||
fmt.Fprintf(fileName, "%s (%s)", file.FileName, converter.ConvertFileSize(file.FileSize, 2))
|
||||
} else {
|
||||
fmt.Printf("%v%v %v\n", indentPrefixStr, prefix, file.FileName)
|
||||
fmt.Fprintf(fileName, "%s", file.FileName)
|
||||
}
|
||||
|
||||
// 文件完整路径
|
||||
if setting.showFullPath {
|
||||
fmt.Printf("%v%v %v -> %s\n", indentPrefixStr, prefix, fileName.String(), targetPathInfo.Path+"/"+file.FileName)
|
||||
} else {
|
||||
fmt.Printf("%v%v %v\n", indentPrefixStr, prefix, fileName.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +212,7 @@ func getTree(driveId, pathStr string, depth int, statistic *treeStatistic, showF
|
||||
}
|
||||
|
||||
// RunTree 列出树形图
|
||||
func RunTree(driveId, pathStr string, showFullPath bool) {
|
||||
func RunTree(driveId, pathStr string, showFullPath, showFileSize bool, minSize, maxSize int64) {
|
||||
activeUser := config.Config.ActiveUser()
|
||||
activeUser.PanClient().ClearCache()
|
||||
activeUser.PanClient().EnableCache()
|
||||
@@ -164,6 +223,12 @@ func RunTree(driveId, pathStr string, showFullPath bool) {
|
||||
CountOfFile: 0,
|
||||
SizeOfFile: 0,
|
||||
}
|
||||
getTree(driveId, pathStr, 0, statistic, showFullPath)
|
||||
setting := &treeConfig{
|
||||
showFullPath: showFullPath,
|
||||
showFileSize: showFileSize,
|
||||
minFileSize: minSize,
|
||||
maxFileSize: maxSize,
|
||||
}
|
||||
getTree(driveId, pathStr, 0, statistic, setting)
|
||||
fmt.Printf("\n%d 个文件夹, %d 个文件, %s 总大小\n", statistic.CountOfDir, statistic.CountOfFile, converter.ConvertFileSize(statistic.SizeOfFile, 2))
|
||||
}
|
||||
|
||||
+43
-14
@@ -4,7 +4,7 @@
|
||||
// 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
|
||||
// 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,
|
||||
@@ -17,6 +17,7 @@ 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"
|
||||
@@ -25,6 +26,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/tickstep/library-go/logger"
|
||||
@@ -106,7 +108,7 @@ var UploadFlags = []cli.Flag{
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "bs",
|
||||
Usage: "block size,上传分片大小,单位KB。推荐值:1024 ~ 10240",
|
||||
Usage: "block size,上传分片大小,单位KB。推荐值:1024 ~ 10240。当上传极大单文件时候请适当调高该值",
|
||||
Value: 10240,
|
||||
},
|
||||
}
|
||||
@@ -131,19 +133,22 @@ func CmdUpload() cli.Command {
|
||||
3. 将本地的 C:\Users\Administrator\Desktop 整个目录上传到网盘 /视频 目录
|
||||
aliyunpan upload C:/Users/Administrator/Desktop /视频
|
||||
|
||||
4. 使用相对路径
|
||||
4. 将本地 200GB 极大文件 C:\Users\Administrator\Desktop\1.mp4 上传到网盘 /视频 目录,需要调高上传分片大小
|
||||
aliyunpan upload -bs 30720 C:/Users/Administrator/Desktop/1.mp4 /视频
|
||||
|
||||
5. 使用相对路径
|
||||
aliyunpan upload 1.mp4 /视频
|
||||
|
||||
5. 覆盖上传,已存在的同名文件会被移到回收站
|
||||
6. 覆盖上传,已存在的同名文件会被移到回收站
|
||||
aliyunpan upload -ow 1.mp4 /视频
|
||||
|
||||
6. 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的.jpg文件
|
||||
7. 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的.jpg文件
|
||||
aliyunpan upload -exn "\.jpg$" C:/Users/Administrator/Video /视频
|
||||
|
||||
7. 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的.jpg文件和.mp3文件,每一个排除项就是一个exn参数
|
||||
8. 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的.jpg文件和.mp3文件,每一个排除项就是一个exn参数
|
||||
aliyunpan upload -exn "\.jpg$" -exn "\.mp3$" C:/Users/Administrator/Video /视频
|
||||
|
||||
8. 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的 @eadir 文件夹
|
||||
9. 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的 @eadir 文件夹
|
||||
aliyunpan upload -exn "^@eadir$" C:/Users/Administrator/Video /视频
|
||||
|
||||
参考:
|
||||
@@ -155,7 +160,7 @@ func CmdUpload() cli.Command {
|
||||
5)排除 myfile.txt 文件:-exn "^myfile.txt$"
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: cmder.ReloadConfigFunc,
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() < 2 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
@@ -172,6 +177,14 @@ func CmdUpload() cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
// 获取上传文件锁,保证上传操作单实例
|
||||
//locker := filelocker.NewFileLocker(config.GetLockerDir() + "/aliyunpan-upload")
|
||||
//if e := filelocker.LockFile(locker, 0755, true, 5*time.Second); e != nil {
|
||||
// logger.Verboseln(e)
|
||||
// fmt.Println("本应用其他实例正在执行上传,请先停止或者等待其完成")
|
||||
// return nil
|
||||
//}
|
||||
|
||||
RunUpload(subArgs[:c.NArg()-1], subArgs[c.NArg()-1], &UploadOptions{
|
||||
AllParallel: c.Int("p"), // 多文件上传的时候,允许同时并行上传的文件数量
|
||||
Parallel: 1, // 一个文件同时多少个线程并发上传的数量。阿里云盘只支持单线程按顺序进行文件part数据上传,所以只能是1
|
||||
@@ -184,6 +197,11 @@ func CmdUpload() cli.Command {
|
||||
ExcludeNames: c.StringSlice("exn"),
|
||||
BlockSize: int64(c.Int("bs") * 1024),
|
||||
})
|
||||
|
||||
// 释放文件锁
|
||||
//if locker != nil {
|
||||
// filelocker.UnlockFile(locker)
|
||||
//}
|
||||
return nil
|
||||
},
|
||||
Flags: UploadFlags,
|
||||
@@ -214,7 +232,7 @@ func CmdRapidUpload() cli.Command {
|
||||
aliyunpan rapidupload "aliyunpan://file.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|pan_folder" "aliyunpan://file1.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|pan_folder"
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: cmder.ReloadConfigFunc,
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() <= 0 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
@@ -250,14 +268,19 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
|
||||
activeUser.PanClient().ClearCache()
|
||||
defer activeUser.PanClient().DisableCache()
|
||||
// pan token expired checker
|
||||
go func() {
|
||||
for {
|
||||
continueFlag := int32(0)
|
||||
atomic.StoreInt32(&continueFlag, 0)
|
||||
defer func() {
|
||||
atomic.StoreInt32(&continueFlag, 1)
|
||||
}()
|
||||
go func(flag *int32) {
|
||||
for atomic.LoadInt32(flag) == 0 {
|
||||
time.Sleep(time.Duration(1) * time.Minute)
|
||||
if RefreshTokenInNeed(activeUser) {
|
||||
if RefreshTokenInNeed(activeUser, config.Config.DeviceName) {
|
||||
logger.Verboseln("update access token for upload task")
|
||||
}
|
||||
}
|
||||
}()
|
||||
}(&continueFlag)
|
||||
|
||||
if opt == nil {
|
||||
opt = &UploadOptions{}
|
||||
@@ -271,7 +294,7 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
|
||||
}
|
||||
}
|
||||
if opt.AllParallel > config.MaxFileUploadParallelNum {
|
||||
opt.AllParallel = config.MaxFileUploadParallelNum
|
||||
fmt.Printf("警告: 当前上传文件并发数过大,可能会被阿里风控导致上传失败,建议调小。\n")
|
||||
}
|
||||
|
||||
if opt.Parallel <= 0 {
|
||||
@@ -330,6 +353,9 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
|
||||
// 获取当前插件
|
||||
plugin, _ := pluginManger.GetPlugin()
|
||||
|
||||
// 上传记录器
|
||||
fileRecorder := log.NewFileRecorder(config.GetLogDir() + "/upload_file_records.csv")
|
||||
|
||||
// 遍历指定的文件并创建上传任务
|
||||
for _, curPath := range localPaths {
|
||||
var walkFunc localfile.MyWalkFunc
|
||||
@@ -417,6 +443,7 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
|
||||
IsOverwrite: opt.IsOverwrite,
|
||||
UseInternalUrl: opt.UseInternalUrl,
|
||||
GlobalSpeedsStat: globalSpeedsStat,
|
||||
FileRecorder: fileRecorder,
|
||||
}, opt.MaxRetry)
|
||||
fmt.Printf("[%s] 加入上传队列: %s\n", taskinfo.Id(), file.LogicPath)
|
||||
} else {
|
||||
@@ -424,6 +451,7 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
|
||||
// 这样空文件夹也可以正确上传
|
||||
saveFilePath := subSavePath
|
||||
if saveFilePath != "/" {
|
||||
folderCreateMutex.Lock()
|
||||
fmt.Printf("正在检测和创建云盘文件夹: %s\n", saveFilePath)
|
||||
_, apierr1 := activeUser.PanClient().FileInfoByPath(opt.DriveId, saveFilePath)
|
||||
time.Sleep(1 * time.Second)
|
||||
@@ -434,6 +462,7 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
|
||||
fmt.Printf("创建云盘文件夹失败: %s\n", saveFilePath)
|
||||
}
|
||||
}
|
||||
folderCreateMutex.Unlock()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -29,7 +29,7 @@ func CmdLoglist() cli.Command {
|
||||
Usage: "列出帐号列表",
|
||||
Description: "列出所有已登录的阿里账号",
|
||||
Category: "阿里云盘账号",
|
||||
Before: cmder.ReloadConfigFunc,
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
fmt.Println(config.Config.UserList.String())
|
||||
return nil
|
||||
@@ -50,8 +50,8 @@ func CmdSu() cli.Command {
|
||||
aliyunpan su <uid or name>
|
||||
`,
|
||||
Category: "阿里云盘账号",
|
||||
Before: cmder.ReloadConfigFunc,
|
||||
After: cmder.SaveConfigFunc,
|
||||
Before: ReloadConfigFunc,
|
||||
After: SaveConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() >= 2 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
@@ -102,7 +102,7 @@ func CmdSu() cli.Command {
|
||||
}
|
||||
|
||||
if switchedUser == nil {
|
||||
switchedUser = cmder.TryLogin()
|
||||
switchedUser = TryLogin()
|
||||
}
|
||||
|
||||
if switchedUser != nil {
|
||||
@@ -122,7 +122,7 @@ func CmdWho() cli.Command {
|
||||
Usage: "获取当前帐号",
|
||||
Description: "获取当前帐号的信息",
|
||||
Category: "阿里云盘账号",
|
||||
Before: cmder.ReloadConfigFunc,
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
|
||||
+164
-10
@@ -16,20 +16,45 @@ package command
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdliner"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/functions/panlogin"
|
||||
"github.com/tickstep/aliyunpan/internal/plugins"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/urfave/cli"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
panCommandVerbose = logger.New("PANCOMMAND", config.EnvVerbose)
|
||||
|
||||
saveConfigMutex *sync.Mutex = new(sync.Mutex)
|
||||
|
||||
ReloadConfigFunc = func(c *cli.Context) error {
|
||||
err := config.Config.Reload()
|
||||
if err != nil {
|
||||
fmt.Printf("重载配置错误: %s\n", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
SaveConfigFunc = func(c *cli.Context) error {
|
||||
saveConfigMutex.Lock()
|
||||
defer saveConfigMutex.Unlock()
|
||||
err := config.Config.Save()
|
||||
if err != nil {
|
||||
fmt.Printf("保存配置错误: %s\n", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
)
|
||||
|
||||
// RunTestShellPattern 执行测试通配符
|
||||
@@ -104,7 +129,7 @@ func UnescapeStr(s string) string {
|
||||
}
|
||||
|
||||
// RefreshTokenInNeed 刷新refresh token
|
||||
func RefreshTokenInNeed(activeUser *config.PanUser) bool {
|
||||
func RefreshTokenInNeed(activeUser *config.PanUser, deviceName string) bool {
|
||||
if activeUser == nil {
|
||||
return false
|
||||
}
|
||||
@@ -116,27 +141,46 @@ func RefreshTokenInNeed(activeUser *config.PanUser) bool {
|
||||
expiredTime, _ := time.ParseInLocation("2006-01-02 15:04:05", activeUser.WebToken.ExpireTime, cz)
|
||||
now := time.Now()
|
||||
if (expiredTime.Unix() - now.Unix()) <= (20 * 60) { // 20min
|
||||
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
|
||||
plugin, _ := pluginManger.GetPlugin()
|
||||
params := &plugins.UserTokenRefreshFinishParams{
|
||||
Result: "success",
|
||||
Message: "",
|
||||
OldToken: "",
|
||||
NewToken: "",
|
||||
UpdatedAt: utils.NowTimeStr(),
|
||||
}
|
||||
|
||||
// need update refresh token
|
||||
logger.Verboseln("access token expired, get new from refresh token")
|
||||
if wt, er := aliyunpan.GetAccessTokenFromRefreshToken(activeUser.RefreshToken); er == nil {
|
||||
params.Result = "success"
|
||||
params.OldToken = activeUser.RefreshToken
|
||||
params.NewToken = wt.RefreshToken
|
||||
|
||||
activeUser.RefreshToken = wt.RefreshToken
|
||||
activeUser.WebToken = *wt
|
||||
activeUser.PanClient().UpdateToken(*wt)
|
||||
logger.Verboseln("get new access token success")
|
||||
|
||||
// plugin callback
|
||||
if er1 := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(activeUser), params); er1 != nil {
|
||||
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er1.Error())
|
||||
}
|
||||
|
||||
// create new signature
|
||||
_, e := activeUser.PanClient().CreateSession(nil)
|
||||
if e != nil {
|
||||
logger.Verboseln("call CreateSession error in RefreshTokenInNeed: " + e.Error())
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
// token refresh error
|
||||
// if token has expired, callback plugin api for notify
|
||||
if now.Unix() >= expiredTime.Unix() {
|
||||
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
|
||||
plugin, _ := pluginManger.GetPlugin()
|
||||
params := &plugins.UserTokenRefreshFinishParams{
|
||||
Result: "fail",
|
||||
Message: er.Error(),
|
||||
OldToken: activeUser.RefreshToken,
|
||||
NewToken: "",
|
||||
UpdatedAt: utils.NowTimeStr(),
|
||||
}
|
||||
params.Result = "fail"
|
||||
params.Message = er.Error()
|
||||
params.OldToken = activeUser.RefreshToken
|
||||
if er1 := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(activeUser), params); er1 != nil {
|
||||
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er1.Error())
|
||||
}
|
||||
@@ -160,3 +204,113 @@ func isIncludeFile(pattern string, fileName string) bool {
|
||||
func isMatchWildcardPattern(name string) bool {
|
||||
return strings.ContainsAny(name, "*") || strings.ContainsAny(name, "?") || strings.ContainsAny(name, "[")
|
||||
}
|
||||
|
||||
// DoLoginHelper 登录助手,使用token进行登录
|
||||
func DoLoginHelper(refreshToken string) (refreshTokenStr string, webToken aliyunpan.WebLoginToken, error error) {
|
||||
line := cmdliner.NewLiner()
|
||||
defer line.Close()
|
||||
|
||||
if refreshToken == "" {
|
||||
refreshToken, error = line.State.Prompt("请输入RefreshToken, 回车键提交 > ")
|
||||
if error != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// app login
|
||||
atoken, apperr := aliyunpan.GetAccessTokenFromRefreshToken(refreshToken)
|
||||
if apperr != nil {
|
||||
if apperr.Code == apierror.ApiCodeTokenExpiredCode || apperr.Code == apierror.ApiCodeRefreshTokenExpiredCode {
|
||||
fmt.Println("Token过期,需要重新登录")
|
||||
} else {
|
||||
fmt.Println("Token登录失败:", apperr)
|
||||
}
|
||||
return "", webToken, fmt.Errorf("登录失败")
|
||||
}
|
||||
refreshTokenStr = refreshToken
|
||||
return refreshTokenStr, *atoken, nil
|
||||
}
|
||||
|
||||
// TryLogin 尝试登录,基础应用支持的各类登录方式进行尝试成功登录
|
||||
func TryLogin() *config.PanUser {
|
||||
// 获取当前插件
|
||||
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
|
||||
plugin, _ := pluginManger.GetPlugin()
|
||||
params := &plugins.UserTokenRefreshFinishParams{
|
||||
Result: "success",
|
||||
Message: "",
|
||||
OldToken: "",
|
||||
NewToken: "",
|
||||
UpdatedAt: utils.NowTimeStr(),
|
||||
}
|
||||
|
||||
// can do automatically login?
|
||||
for _, u := range config.Config.UserList {
|
||||
if u.UserId == config.Config.ActiveUID {
|
||||
// login
|
||||
_, webToken, err := DoLoginHelper(u.RefreshToken)
|
||||
if err != nil {
|
||||
logger.Verboseln("automatically login use saved refresh token error ", err)
|
||||
if u.TokenId != "" {
|
||||
logger.Verboseln("try to login use tokenId")
|
||||
h := panlogin.NewLoginHelper(config.DefaultTokenServiceWebHost)
|
||||
r, e := h.GetRefreshToken(u.TokenId)
|
||||
if e != nil {
|
||||
logger.Verboseln("try to login use tokenId error", e)
|
||||
// login fail plugin callback
|
||||
params.Result = "fail"
|
||||
params.OldToken = u.RefreshToken
|
||||
params.NewToken = ""
|
||||
if er := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(u), params); er != nil {
|
||||
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er.Error())
|
||||
}
|
||||
break
|
||||
}
|
||||
refreshToken, e := h.ParseSecureRefreshToken("", r.SecureRefreshToken)
|
||||
if e != nil {
|
||||
logger.Verboseln("try to parse refresh token error", e)
|
||||
break
|
||||
}
|
||||
_, webToken, err = DoLoginHelper(refreshToken)
|
||||
if err != nil {
|
||||
logger.Verboseln("try to use refresh token from tokenId error", e)
|
||||
break
|
||||
}
|
||||
fmt.Println("Token重新自动登录成功")
|
||||
// save new refresh token
|
||||
u.RefreshToken = refreshToken
|
||||
} else {
|
||||
// login fail plugin callback
|
||||
params.Result = "fail"
|
||||
params.OldToken = u.RefreshToken
|
||||
params.NewToken = ""
|
||||
if er := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(u), params); er != nil {
|
||||
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er.Error())
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
// plugin param
|
||||
params.Result = "success"
|
||||
params.OldToken = u.RefreshToken
|
||||
params.NewToken = webToken.RefreshToken
|
||||
|
||||
// success login, save new token and access token
|
||||
u.RefreshToken = webToken.RefreshToken
|
||||
u.WebToken = webToken
|
||||
|
||||
// save
|
||||
SaveConfigFunc(nil)
|
||||
// reload
|
||||
ReloadConfigFunc(nil)
|
||||
|
||||
// do plugin callback
|
||||
if er := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(u), params); er != nil {
|
||||
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er.Error())
|
||||
}
|
||||
|
||||
return config.Config.ActiveUser()
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/urfave/cli"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -30,8 +31,8 @@ func CmdWebdav() cli.Command {
|
||||
Usage: "在线网盘服务",
|
||||
Description: "webdav在线网盘服务",
|
||||
Category: "阿里云盘",
|
||||
Before: cmder.ReloadConfigFunc,
|
||||
After: cmder.SaveConfigFunc,
|
||||
Before: ReloadConfigFunc,
|
||||
After: SaveConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
fmt.Print(`
|
||||
本文命令可以让阿里云盘变身为webdav协议的文件服务器。这样你可以把阿里云盘挂载为Windows、Linux、Mac系统的磁盘,可以通过NAS系统做文件管理或文件同步等等。
|
||||
@@ -72,16 +73,23 @@ aliyunpan webdav start -h
|
||||
return nil
|
||||
}
|
||||
activeUser := GetActiveUser()
|
||||
go func() {
|
||||
for {
|
||||
|
||||
// pan token expired checker
|
||||
continueFlag := int32(0)
|
||||
atomic.StoreInt32(&continueFlag, 0)
|
||||
defer func() {
|
||||
atomic.StoreInt32(&continueFlag, 1)
|
||||
}()
|
||||
go func(flag *int32) {
|
||||
for atomic.LoadInt32(flag) == 0 {
|
||||
// token刷新
|
||||
time.Sleep(time.Duration(1) * time.Minute)
|
||||
//time.Sleep(time.Duration(5) * time.Second)
|
||||
if RefreshTokenInNeed(activeUser) {
|
||||
if RefreshTokenInNeed(activeUser, config.Config.DeviceName) {
|
||||
logger.Verboseln("reload new access token for webdav")
|
||||
}
|
||||
}
|
||||
}()
|
||||
}(&continueFlag)
|
||||
|
||||
webdavServ := &webdav.WebdavConfig{
|
||||
PanDriveId: "",
|
||||
|
||||
@@ -0,0 +1,178 @@
|
||||
// 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 command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/urfave/cli"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func CmdXcp() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "xcp",
|
||||
Usage: "备份盘和资源库之间转存文件",
|
||||
UsageText: `
|
||||
aliyunpan xcp <文件/目录1> <文件/目录2> <文件/目录3> ... <目标盘目录>`,
|
||||
Description: `
|
||||
注意: 拷贝多个文件和目录时, 请确保每一个文件和目录都存在, 否则拷贝操作会失败。
|
||||
|
||||
示例:
|
||||
|
||||
当前程序工作在备份盘下,将备份盘 /1.mp4 文件转存复制到 资源库下的 /来自备份盘/video 目录下
|
||||
aliyunpan xcp /1.mp4 /来自备份盘/video
|
||||
|
||||
当前程序工作在资源库下,将资源库 /1.mp4 文件转存复制到 备份盘下的 /来自资源库/video 目录下
|
||||
aliyunpan xcp /1.mp4 /来自资源库/video
|
||||
|
||||
当前程序工作在备份盘下,将 /我的资源 目录下所有的.mp4文件 复制到 /来自备份盘/video 目录下面,使用通配符匹配
|
||||
aliyunpan xcp /我的资源/*.mp4 /来自备份盘/video
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() <= 0 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
srcDriveId := parseDriveId(c)
|
||||
dstDriveId := ""
|
||||
driveList := config.Config.ActiveUser().DriveList
|
||||
if driveList.GetFileDriveId() == srcDriveId {
|
||||
dstDriveId = driveList.GetResourceDriveId()
|
||||
} else if driveList.GetResourceDriveId() == srcDriveId {
|
||||
dstDriveId = driveList.GetFileDriveId()
|
||||
} else {
|
||||
fmt.Println("不支持该操作")
|
||||
return nil
|
||||
}
|
||||
RunXCopy(srcDriveId, dstDriveId, c.Args()...)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "driveId",
|
||||
Usage: "源网盘ID",
|
||||
Value: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RunXCopy 执行复制文件/目录
|
||||
func RunXCopy(srcDriveId, dstDriveId string, paths ...string) {
|
||||
activeUser := GetActiveUser()
|
||||
cacheCleanPaths := []string{}
|
||||
opFileList, targetFile, _, err := getCrossCopyFileInfo(srcDriveId, dstDriveId, paths...)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
if targetFile == nil {
|
||||
fmt.Println("目标目录不存在")
|
||||
return
|
||||
}
|
||||
if opFileList == nil || len(opFileList) == 0 {
|
||||
fmt.Println("没有有效的文件可复制")
|
||||
return
|
||||
}
|
||||
cacheCleanPaths = append(cacheCleanPaths, targetFile.Path)
|
||||
|
||||
failedCopyFiles := []*aliyunpan.FileEntity{}
|
||||
copyFileParamList := []string{}
|
||||
fileId2FileEntity := map[string]*aliyunpan.FileEntity{}
|
||||
for _, mfi := range opFileList {
|
||||
fileId2FileEntity[mfi.FileId] = mfi
|
||||
copyFileParamList = append(copyFileParamList, mfi.FileId)
|
||||
cacheCleanPaths = append(cacheCleanPaths, path.Dir(mfi.Path))
|
||||
}
|
||||
fccr, er := activeUser.PanClient().FileCrossDriveCopy(&aliyunpan.FileCrossCopyParam{
|
||||
FromDriveId: srcDriveId,
|
||||
FromFileIds: copyFileParamList,
|
||||
ToDriveId: dstDriveId,
|
||||
ToParentFileId: targetFile.FileId,
|
||||
})
|
||||
for _, rs := range fccr {
|
||||
if rs.Status != 201 {
|
||||
failedCopyFiles = append(failedCopyFiles, fileId2FileEntity[rs.SourceFileId])
|
||||
}
|
||||
}
|
||||
|
||||
if len(failedCopyFiles) > 0 {
|
||||
fmt.Println("以下文件复制失败:")
|
||||
for _, f := range failedCopyFiles {
|
||||
fmt.Println(f.FileName)
|
||||
}
|
||||
fmt.Println("")
|
||||
}
|
||||
if er == nil {
|
||||
pnt := func() {
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
tb.SetHeader([]string{"#", "文件/目录"})
|
||||
for k, rs := range fccr {
|
||||
tb.Append([]string{strconv.Itoa(k + 1), fileId2FileEntity[rs.SourceFileId].Path})
|
||||
}
|
||||
tb.Render()
|
||||
}
|
||||
fmt.Println("操作成功, 以下文件已复制到目标目录: ", targetFile.Path)
|
||||
pnt()
|
||||
activeUser.DeleteCache(cacheCleanPaths)
|
||||
} else {
|
||||
fmt.Println("无法复制文件,请稍后重试")
|
||||
}
|
||||
}
|
||||
|
||||
func getCrossCopyFileInfo(srcDriveId, dstDriveId string, paths ...string) (opFileList []*aliyunpan.FileEntity, targetFile *aliyunpan.FileEntity, failedPaths []string, error error) {
|
||||
if len(paths) <= 1 {
|
||||
return nil, nil, nil, fmt.Errorf("请指定目标文件夹路径")
|
||||
}
|
||||
activeUser := GetActiveUser()
|
||||
|
||||
// the last one is the target file path
|
||||
targetFilePath := path.Clean(paths[len(paths)-1])
|
||||
absolutePath := activeUser.PathJoin(dstDriveId, targetFilePath)
|
||||
targetFile, err := activeUser.PanClient().FileInfoByPath(dstDriveId, absolutePath)
|
||||
if err != nil || !targetFile.IsFolder() {
|
||||
return nil, nil, nil, fmt.Errorf("指定目标文件夹不存在")
|
||||
}
|
||||
|
||||
// source file list
|
||||
for idx := 0; idx < (len(paths) - 1); idx++ {
|
||||
absolutePath = path.Clean(activeUser.PathJoin(srcDriveId, paths[idx]))
|
||||
fileList, err1 := matchPathByShellPattern(srcDriveId, absolutePath)
|
||||
if err1 != nil {
|
||||
failedPaths = append(failedPaths, absolutePath)
|
||||
continue
|
||||
}
|
||||
if fileList == nil || len(fileList) == 0 {
|
||||
// 文件不存在
|
||||
failedPaths = append(failedPaths, absolutePath)
|
||||
continue
|
||||
}
|
||||
for _, f := range fileList {
|
||||
// 移动匹配的文件
|
||||
opFileList = append(opFileList, f)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
// 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
|
||||
// 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,
|
||||
@@ -58,6 +58,9 @@ const (
|
||||
|
||||
// DefaultVideoFileExtensions 默认的视频文件后缀
|
||||
DefaultVideoFileExtensions = "mp4,flv,mkv,mov,rm,rmvb,wmv,wma,mv,asf,asx,mpg,mpeg,mpe,3gp,m4v,avi,vob"
|
||||
|
||||
// DefaultDeviceName 默认客户端名称
|
||||
DefaultDeviceName = "Chrome浏览器"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -101,6 +104,10 @@ type PanConfig struct {
|
||||
UpdateCheckInfo UpdateCheckInfo `json:"updateCheckInfo"`
|
||||
|
||||
VideoFileExtensions string `json:"videoFileExtensions"`
|
||||
FileRecordConfig string `json:"fileRecordConfig"` // 上传、下载、同步文件的记录,包括失败和成功的
|
||||
|
||||
DeviceId string `json:"deviceId"` // 客户端ID,用于标识登录客户端,阿里单个账号最多允许10个客户端同时登录
|
||||
DeviceName string `json:"deviceName"` // 客户端名称,默认为:Chrome浏览器
|
||||
|
||||
configFilePath string
|
||||
configFile *os.File
|
||||
@@ -250,6 +257,9 @@ func (c *PanConfig) loadConfigFromFile() (err error) {
|
||||
if err != nil {
|
||||
return ErrConfigContentsParseError
|
||||
}
|
||||
if c.DeviceName == "" {
|
||||
c.DeviceName = DefaultDeviceName
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -272,6 +282,9 @@ func (c *PanConfig) initDefaultConfig() {
|
||||
}
|
||||
c.ConfigVer = ConfigVersion
|
||||
c.VideoFileExtensions = DefaultVideoFileExtensions
|
||||
c.DeviceId = RandomDeviceId() // 生成默认客户端ID
|
||||
c.DeviceName = DefaultDeviceName
|
||||
c.FileRecordConfig = "1" // 默认开启
|
||||
}
|
||||
|
||||
// GetConfigDir 获取配置路径
|
||||
@@ -349,6 +362,11 @@ func GetLogFilePath() string {
|
||||
return dirPath + "/" + "aliyunpan_verbose.log"
|
||||
}
|
||||
|
||||
// GetLockerDir 获取文件锁路径
|
||||
func GetLockerDir() string {
|
||||
return strings.TrimSuffix(GetConfigDir(), "/")
|
||||
}
|
||||
|
||||
func (c *PanConfig) ActiveUser() *PanUser {
|
||||
if c.activeUser == nil {
|
||||
if c.UserList == nil {
|
||||
@@ -361,7 +379,7 @@ func (c *PanConfig) ActiveUser() *PanUser {
|
||||
if u.UserId == c.ActiveUID {
|
||||
if u.PanClient() == nil {
|
||||
// restore client
|
||||
user, err := SetupUserByCookie(&u.WebToken)
|
||||
user, err := SetupUserByCookie(&u.WebToken, c.DeviceId, c.DeviceName)
|
||||
if err != nil {
|
||||
logger.Verboseln("setup user error")
|
||||
return nil
|
||||
@@ -380,6 +398,15 @@ func (c *PanConfig) ActiveUser() *PanUser {
|
||||
} else {
|
||||
u.WorkdirFileEntity = *fe
|
||||
}
|
||||
} else if user.IsResourceDriveActive() {
|
||||
fe, err1 := u.PanClient().FileInfoByPath(u.ActiveDriveId, u.ResourceWorkdir)
|
||||
if err1 != nil {
|
||||
// default to root
|
||||
u.ResourceWorkdir = "/"
|
||||
u.ResourceWorkdirFileEntity = *aliyunpan.NewFileEntityForRootDir()
|
||||
} else {
|
||||
u.ResourceWorkdirFileEntity = *fe
|
||||
}
|
||||
} else if user.IsAlbumDriveActive() {
|
||||
fe, err1 := u.PanClient().FileInfoByPath(u.ActiveDriveId, u.AlbumWorkdir)
|
||||
if err1 != nil {
|
||||
|
||||
@@ -66,8 +66,29 @@ func (c *PanConfig) SetMaxUploadRateByStr(sizeStr string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetFileRecorderConfig 设置文件记录器
|
||||
func (c *PanConfig) SetFileRecorderConfig(config string) error {
|
||||
if config == "1" || config == "2" {
|
||||
c.FileRecordConfig = config
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetDeviceId 设置客户端ID
|
||||
func (c *PanConfig) SetDeviceId(deviceId string) error {
|
||||
if deviceId == "" {
|
||||
return nil
|
||||
}
|
||||
c.DeviceId = deviceId
|
||||
return nil
|
||||
}
|
||||
|
||||
// PrintTable 输出表格
|
||||
func (c *PanConfig) PrintTable() {
|
||||
fileRecorderLabel := "禁用"
|
||||
if c.FileRecordConfig == "1" {
|
||||
fileRecorderLabel = "开启"
|
||||
}
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
tb.SetHeader([]string{"名称", "值", "建议值", "描述"})
|
||||
tb.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
|
||||
@@ -80,8 +101,10 @@ func (c *PanConfig) PrintTable() {
|
||||
[]string{"max_upload_rate", showMaxRate(c.MaxUploadRate), "", "限制单个文件最大上传速度, 0代表不限制"},
|
||||
[]string{"transfer_url_type", strconv.Itoa(c.TransferUrlType), "1-默认,2-阿里云ECS", "上传下载URL类别。除非在阿里云ECS(暂只支持经典网络)服务器中使用,不然请设置1"},
|
||||
[]string{"savedir", c.SaveDir, "", "下载文件的储存目录"},
|
||||
[]string{"proxy", c.Proxy, "", "设置代理, 支持 http/socks5 代理,例如:http://127.0.0.1:8888"},
|
||||
[]string{"local_addrs", c.LocalAddrs, "", "设置本地网卡地址, 多个地址用逗号隔开,例如:127.0.0.1,192.168.100.126"},
|
||||
[]string{"proxy", c.Proxy, "", "设置代理, 支持 http/socks5 代理,例如: http://127.0.0.1:8888 或者 socks5://127.0.0.1:8889"},
|
||||
[]string{"local_addrs", c.LocalAddrs, "", "设置本地网卡地址, 多个地址用逗号隔开,例如: 127.0.0.1,192.168.100.126"},
|
||||
[]string{"file_record_config", fileRecorderLabel, "1-开启,2-禁用", "设置是否开启上传、下载、同步文件的结果记录,开启后会把结果记录到CSV文件方便后期查看"},
|
||||
[]string{"device_id", c.DeviceId, "", "客户端ID,用于标识登录客户端,阿里单个账号最多允许10个客户端同时在线。修改后需要重启应用生效"},
|
||||
})
|
||||
tb.Render()
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// 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
|
||||
// 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,
|
||||
@@ -48,14 +48,29 @@ func (d DriveInfoList) GetAlbumDriveId() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (d DriveInfoList) GetResourceDriveId() string {
|
||||
for _, drive := range d {
|
||||
if drive.DriveTag == "Resource" {
|
||||
return drive.DriveId
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type PanUser struct {
|
||||
UserId string `json:"userId"`
|
||||
Nickname string `json:"nickname"`
|
||||
AccountName string `json:"accountName"`
|
||||
|
||||
// 文件/备份盘
|
||||
Workdir string `json:"workdir"`
|
||||
WorkdirFileEntity aliyunpan.FileEntity `json:"workdirFileEntity"`
|
||||
|
||||
// 资源库
|
||||
ResourceWorkdir string `json:"resourceWorkdir"`
|
||||
ResourceWorkdirFileEntity aliyunpan.FileEntity `json:"resourceWorkdirFileEntity"`
|
||||
|
||||
// 相册
|
||||
AlbumWorkdir string `json:"albumWorkdir"`
|
||||
AlbumWorkdirFileEntity aliyunpan.FileEntity `json:"albumWorkdirFileEntity"`
|
||||
|
||||
@@ -72,7 +87,7 @@ type PanUser struct {
|
||||
|
||||
type PanUserList []*PanUser
|
||||
|
||||
func SetupUserByCookie(webToken *aliyunpan.WebLoginToken) (user *PanUser, err *apierror.ApiError) {
|
||||
func SetupUserByCookie(webToken *aliyunpan.WebLoginToken, deviceId, deviceName string) (user *PanUser, err *apierror.ApiError) {
|
||||
tryRefreshWebToken := true
|
||||
|
||||
if webToken == nil {
|
||||
@@ -80,7 +95,17 @@ func SetupUserByCookie(webToken *aliyunpan.WebLoginToken) (user *PanUser, err *a
|
||||
}
|
||||
|
||||
doLoginAct:
|
||||
panClient := aliyunpan.NewPanClient(*webToken, aliyunpan.AppLoginToken{})
|
||||
appConfig := aliyunpan.AppConfig{
|
||||
AppId: "25dzX3vbYqktVxyX",
|
||||
DeviceId: deviceId,
|
||||
UserId: "",
|
||||
Nonce: 0,
|
||||
PublicKey: "",
|
||||
}
|
||||
panClient := aliyunpan.NewPanClient(*webToken, aliyunpan.AppLoginToken{}, appConfig, aliyunpan.SessionConfig{
|
||||
DeviceName: deviceName,
|
||||
ModelName: "Windows网页版",
|
||||
})
|
||||
u := &PanUser{
|
||||
WebToken: *webToken,
|
||||
panClient: panClient,
|
||||
@@ -119,13 +144,26 @@ doLoginAct:
|
||||
|
||||
// drive list
|
||||
u.DriveList = DriveInfoList{
|
||||
{DriveId: userInfo.FileDriveId, DriveTag: "File", DriveName: "文件"},
|
||||
{DriveId: userInfo.FileDriveId, DriveTag: "File", DriveName: "备份盘"},
|
||||
{DriveId: userInfo.ResourceDriveId, DriveTag: "Resource", DriveName: "资源库"},
|
||||
{DriveId: userInfo.AlbumDriveId, DriveTag: "Album", DriveName: "相册"},
|
||||
}
|
||||
} else {
|
||||
// error, maybe the token has expired
|
||||
return nil, apierror.NewFailedApiError("cannot get user info, the token has expired")
|
||||
}
|
||||
|
||||
// create session
|
||||
appConfig.UserId = u.UserId
|
||||
panClient.UpdateAppConfig(appConfig)
|
||||
r, e := panClient.CreateSession(nil)
|
||||
if e != nil {
|
||||
logger.Verboseln("call CreateSession error in SetupUserByCookie: " + e.Error())
|
||||
}
|
||||
if r != nil && !r.Result {
|
||||
logger.Verboseln("上传签名秘钥失败,可能是你账号登录的设备已超最大数量")
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
||||
@@ -143,6 +181,8 @@ func (pu *PanUser) PathJoin(driveId, p string) string {
|
||||
if di != nil {
|
||||
if di.IsFileDrive() {
|
||||
wd = pu.Workdir
|
||||
} else if di.IsResourceDrive() {
|
||||
wd = pu.ResourceWorkdir
|
||||
} else if di.IsAlbumDrive() {
|
||||
wd = pu.AlbumWorkdir
|
||||
}
|
||||
@@ -217,6 +257,11 @@ func (pu *PanUser) IsAlbumDriveActive() bool {
|
||||
return d != nil && d.IsAlbumDrive()
|
||||
}
|
||||
|
||||
func (pu *PanUser) IsResourceDriveActive() bool {
|
||||
d := pu.GetActiveDriveInfo()
|
||||
return d != nil && d.IsResourceDrive()
|
||||
}
|
||||
|
||||
func (di *DriveInfo) IsFileDrive() bool {
|
||||
return di.DriveTag == "File"
|
||||
}
|
||||
@@ -224,3 +269,7 @@ func (di *DriveInfo) IsFileDrive() bool {
|
||||
func (di *DriveInfo) IsAlbumDrive() bool {
|
||||
return di.DriveTag == "Album"
|
||||
}
|
||||
|
||||
func (di *DriveInfo) IsResourceDrive() bool {
|
||||
return di.DriveTag == "Resource"
|
||||
}
|
||||
|
||||
@@ -21,9 +21,11 @@ import (
|
||||
"github.com/tickstep/library-go/crypto"
|
||||
"github.com/tickstep/library-go/ids"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (pl *PanUserList) String() string {
|
||||
@@ -121,3 +123,15 @@ func IsFolderExist(pathStr string) bool {
|
||||
}
|
||||
return fi.IsDir()
|
||||
}
|
||||
|
||||
// RandomDeviceId 随机生成device-id
|
||||
func RandomDeviceId() string {
|
||||
count := 24
|
||||
STR_SET := "abcdefjhijklmnopqrstuvwxyzABCDEFJHIJKLMNOPQRSTUVWXYZ1234567890"
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
str := strings.Builder{}
|
||||
for i := 0; i < count; i++ {
|
||||
str.WriteByte(byte(STR_SET[rand.Intn(len(STR_SET))]))
|
||||
}
|
||||
return str.String()
|
||||
}
|
||||
|
||||
@@ -25,3 +25,7 @@ func TestEncryptString(t *testing.T) {
|
||||
func TestDecryptString(t *testing.T) {
|
||||
fmt.Println(DecryptString("75b3c8d21607440c0e8a70f4a4861c8669774cc69c70ce2a2c8acb815b6d5d3b"))
|
||||
}
|
||||
|
||||
func TestRandomDeviceId(t *testing.T) {
|
||||
fmt.Println(RandomDeviceId())
|
||||
}
|
||||
|
||||
@@ -383,6 +383,18 @@ func (der *Downloader) Execute() error {
|
||||
DriveId: der.driveId,
|
||||
FileId: der.fileInfo.FileId,
|
||||
})
|
||||
if apierr != nil && apierr.Code == apierror.ApiCodeDeviceSessionSignatureInvalid {
|
||||
_, e := der.panClient.CreateSession(nil)
|
||||
if e == nil {
|
||||
// retry
|
||||
durl, apierr = der.panClient.GetFileDownloadUrl(&aliyunpan.GetFileDownloadUrlParam{
|
||||
DriveId: der.driveId,
|
||||
FileId: der.fileInfo.FileId,
|
||||
})
|
||||
} else {
|
||||
logger.Verboseln("CreateSession failed")
|
||||
}
|
||||
}
|
||||
time.Sleep(time.Duration(200) * time.Millisecond)
|
||||
if apierr != nil {
|
||||
logger.Verbosef("ERROR: get download url error: %s\n", der.fileInfo.FileId)
|
||||
|
||||
@@ -13,12 +13,21 @@
|
||||
// limitations under the License.
|
||||
package uploader
|
||||
|
||||
import "fmt"
|
||||
|
||||
var (
|
||||
UploadUrlExpired = fmt.Errorf("UrlExpired")
|
||||
UploadPartNotSeq = fmt.Errorf("PartNotSequential")
|
||||
UploadTerminate = fmt.Errorf("UploadErrorTerminate")
|
||||
UploadPartAlreadyExist = fmt.Errorf("PartAlreadyExist")
|
||||
)
|
||||
|
||||
type (
|
||||
// MultiError 多线程上传的错误
|
||||
MultiError struct {
|
||||
Err error
|
||||
// IsRetry 是否重试,
|
||||
Terminated bool
|
||||
Terminated bool
|
||||
NeedStartOver bool // 是否从头开始上传
|
||||
}
|
||||
)
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/library-go/requester"
|
||||
"github.com/tickstep/library-go/requester/rio"
|
||||
"github.com/tickstep/library-go/requester/rio/speeds"
|
||||
@@ -136,13 +137,13 @@ func (muer *MultiUploader) Execute() error {
|
||||
// 分配任务
|
||||
if muer.instanceState != nil {
|
||||
muer.workers = muer.getWorkerListByInstanceState(muer.instanceState)
|
||||
uploaderVerbose.Infof("upload task CREATED from instance state\n")
|
||||
logger.Verboseln("upload task CREATED from instance state\n")
|
||||
} else {
|
||||
muer.workers = muer.getWorkerListByInstanceState(&InstanceState{
|
||||
BlockList: SplitBlock(muer.file.Len(), muer.config.BlockSize),
|
||||
})
|
||||
|
||||
uploaderVerbose.Infof("upload task CREATED: block size: %d, num: %d\n", muer.config.BlockSize, len(muer.workers))
|
||||
logger.Verboseln("upload task CREATED: block size: %d, num: %d\n", muer.config.BlockSize, len(muer.workers))
|
||||
}
|
||||
|
||||
// 开始上传
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"context"
|
||||
"github.com/oleiade/lane"
|
||||
"github.com/tickstep/aliyunpan/internal/waitgroup"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/library-go/requester"
|
||||
"os"
|
||||
"strconv"
|
||||
@@ -66,77 +67,83 @@ func (muer *MultiUploader) upload() (uperr error) {
|
||||
uploadClient.SetKeepAlive(true)
|
||||
|
||||
for {
|
||||
// 阿里云盘只支持分片按顺序上传,这里正常应该是parallel = 1
|
||||
// 阿里云盘只支持分片按顺序上传,这里必须是parallel = 1
|
||||
wg := waitgroup.NewWaitGroup(muer.config.Parallel)
|
||||
for {
|
||||
e := uploadDeque.Shift()
|
||||
if e == nil { // 任务为空
|
||||
wg.AddDelta()
|
||||
|
||||
uperr = nil
|
||||
e := uploadDeque.Shift()
|
||||
if e == nil { // 任务为空
|
||||
break
|
||||
}
|
||||
|
||||
wer := e.(*worker)
|
||||
go func() { // 异步上传
|
||||
defer wg.Done()
|
||||
|
||||
var (
|
||||
ctx, cancel = context.WithCancel(context.Background())
|
||||
doneChan = make(chan struct{})
|
||||
uploadDone bool
|
||||
terr error
|
||||
)
|
||||
go func() {
|
||||
if !wer.uploadDone {
|
||||
logger.Verboseln("begin to upload part: " + strconv.Itoa(wer.id))
|
||||
uploadDone, terr = muer.multiUpload.UploadFile(ctx, int(wer.id), wer.partOffset, wer.splitUnit.Range().End, wer.splitUnit, uploadClient)
|
||||
} else {
|
||||
uploadDone = true
|
||||
}
|
||||
close(doneChan)
|
||||
}()
|
||||
select { // 监听上传进程,循环阻塞
|
||||
case <-muer.canceled:
|
||||
cancel()
|
||||
return
|
||||
case <-doneChan:
|
||||
// continue
|
||||
logger.Verboseln("multiUpload worker upload file done")
|
||||
}
|
||||
cancel()
|
||||
if terr != nil {
|
||||
logger.Verboseln("upload file part err: %+v", terr)
|
||||
if me, ok := terr.(*MultiError); ok {
|
||||
if me.Terminated { // 终止
|
||||
muer.closeCanceledOnce.Do(func() { // 只关闭一次
|
||||
close(muer.canceled)
|
||||
})
|
||||
uperr = me.Err
|
||||
return
|
||||
} else if me.NeedStartOver {
|
||||
logger.Verboseln("upload start over: %d\n", wer.id)
|
||||
// 从头开始上传
|
||||
muer.closeCanceledOnce.Do(func() { // 只关闭一次
|
||||
close(muer.canceled)
|
||||
})
|
||||
uperr = me.Err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
logger.Verboseln("upload err: %s, id: %d\n", terr, wer.id)
|
||||
wer.splitUnit.Seek(0, os.SEEK_SET)
|
||||
uploadDeque.Prepend(wer) // 放回上传队列首位
|
||||
return
|
||||
}
|
||||
wer.uploadDone = uploadDone
|
||||
|
||||
// 通知更新
|
||||
if muer.updateInstanceStateChan != nil && len(muer.updateInstanceStateChan) < cap(muer.updateInstanceStateChan) {
|
||||
muer.updateInstanceStateChan <- struct{}{}
|
||||
}
|
||||
}()
|
||||
wg.Wait()
|
||||
if uperr != nil {
|
||||
if uperr == UploadPartNotSeq {
|
||||
// 分片出现乱序,需要重新上传,取消本次所有剩余的分片的上传
|
||||
break
|
||||
}
|
||||
|
||||
wer := e.(*worker)
|
||||
wg.AddDelta()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
var (
|
||||
ctx, cancel = context.WithCancel(context.Background())
|
||||
doneChan = make(chan struct{})
|
||||
uploadDone bool
|
||||
terr error
|
||||
)
|
||||
go func() {
|
||||
if !wer.uploadDone {
|
||||
uploaderVerbose.Info("begin to upload part: " + strconv.Itoa(wer.id))
|
||||
uploadDone, terr = muer.multiUpload.UploadFile(ctx, int(wer.id), wer.partOffset, wer.splitUnit.Range().End, wer.splitUnit, uploadClient)
|
||||
} else {
|
||||
uploadDone = true
|
||||
}
|
||||
close(doneChan)
|
||||
}()
|
||||
select {
|
||||
case <-muer.canceled:
|
||||
cancel()
|
||||
return
|
||||
case <-doneChan:
|
||||
// continue
|
||||
uploaderVerbose.Info("multiUpload worker upload file done")
|
||||
}
|
||||
cancel()
|
||||
if terr != nil {
|
||||
if me, ok := terr.(*MultiError); ok {
|
||||
if me.Terminated { // 终止
|
||||
muer.closeCanceledOnce.Do(func() { // 只关闭一次
|
||||
close(muer.canceled)
|
||||
})
|
||||
uperr = me.Err
|
||||
return
|
||||
} else if me.NeedStartOver {
|
||||
uploaderVerbose.Warnf("upload start over: %d\n", wer.id)
|
||||
// 从头开始上传
|
||||
muer.closeCanceledOnce.Do(func() { // 只关闭一次
|
||||
close(muer.canceled)
|
||||
})
|
||||
uperr = me.Err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
uploaderVerbose.Warnf("upload err: %s, id: %d\n", terr, wer.id)
|
||||
wer.splitUnit.Seek(0, os.SEEK_SET)
|
||||
uploadDeque.Append(wer)
|
||||
return
|
||||
}
|
||||
wer.uploadDone = uploadDone
|
||||
|
||||
// 通知更新
|
||||
if muer.updateInstanceStateChan != nil && len(muer.updateInstanceStateChan) < cap(muer.updateInstanceStateChan) {
|
||||
muer.updateInstanceStateChan <- struct{}{}
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
// 没有任务了
|
||||
if uploadDeque.Size() == 0 {
|
||||
break
|
||||
@@ -164,11 +171,11 @@ func (muer *MultiUploader) upload() (uperr error) {
|
||||
if allSuccess {
|
||||
e := muer.multiUpload.CommitFile()
|
||||
if e != nil {
|
||||
uploaderVerbose.Warn("upload file commit failed: " + e.Error())
|
||||
logger.Verboseln("upload file commit failed: " + e.Error())
|
||||
return e
|
||||
}
|
||||
} else {
|
||||
uploaderVerbose.Warn("upload file not all success: " + muer.UploadOpEntity.FileId)
|
||||
logger.Verboseln("upload file not all success: " + muer.UploadOpEntity.FileId)
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
@@ -14,10 +14,8 @@
|
||||
package uploader
|
||||
|
||||
import (
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/library-go/requester"
|
||||
"github.com/tickstep/library-go/requester/rio"
|
||||
"net/http"
|
||||
@@ -51,10 +49,6 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
uploaderVerbose = logger.New("UPLOADER", config.EnvVerbose)
|
||||
)
|
||||
|
||||
// NewUploader 返回 uploader 对象, url: 上传地址, readerlen64: 实现 rio.ReaderLen64 接口的对象, 例如文件
|
||||
func NewUploader(url string, readerlen64 rio.ReaderLen64) (uploader *Uploader) {
|
||||
uploader = &Uploader{
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// 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
|
||||
// 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,
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/tickstep/aliyunpan/internal/file/downloader"
|
||||
"github.com/tickstep/aliyunpan/internal/functions"
|
||||
"github.com/tickstep/aliyunpan/internal/localfile"
|
||||
"github.com/tickstep/aliyunpan/internal/log"
|
||||
"github.com/tickstep/aliyunpan/internal/plugins"
|
||||
"github.com/tickstep/aliyunpan/internal/taskframework"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
@@ -42,6 +43,8 @@ import (
|
||||
)
|
||||
|
||||
type (
|
||||
FileSourceType string
|
||||
|
||||
// DownloadTaskUnit 下载的任务单元
|
||||
DownloadTaskUnit struct {
|
||||
taskInfo *taskframework.TaskInfo // 任务信息
|
||||
@@ -66,7 +69,11 @@ type (
|
||||
OriginSaveRootPath string // 文件保存在本地的根目录路径
|
||||
DriveId string
|
||||
|
||||
fileInfo *aliyunpan.FileEntity // 文件或目录详情
|
||||
fileInfo *aliyunpan.FileEntity // 文件或目录详情
|
||||
downloadFileSource FileSourceType // 下载项类型,File-普通文件(备份盘/资源库),Album-相册文件
|
||||
|
||||
// 下载文件记录器
|
||||
FileRecorder *log.FileRecorder
|
||||
}
|
||||
)
|
||||
|
||||
@@ -85,8 +92,20 @@ const (
|
||||
StrDownloadChecksumFailed = "检测文件有效性失败"
|
||||
// DefaultDownloadMaxRetry 默认下载失败最大重试次数
|
||||
DefaultDownloadMaxRetry = 3
|
||||
|
||||
// BackupFileSource 备份盘文件
|
||||
BackupFileSource FileSourceType = "backup"
|
||||
// ResourceFileSource 资源库文件
|
||||
ResourceFileSource FileSourceType = "resource"
|
||||
// AlbumFileSource 相册文件
|
||||
AlbumFileSource FileSourceType = "album"
|
||||
)
|
||||
|
||||
func (dtu *DownloadTaskUnit) SetFileInfo(fileType FileSourceType, info *aliyunpan.FileEntity) {
|
||||
dtu.downloadFileSource = fileType
|
||||
dtu.fileInfo = info
|
||||
}
|
||||
|
||||
func (dtu *DownloadTaskUnit) SetTaskInfo(info *taskframework.TaskInfo) {
|
||||
dtu.taskInfo = info
|
||||
}
|
||||
@@ -205,9 +224,11 @@ func (dtu *DownloadTaskUnit) download() (err error) {
|
||||
}
|
||||
|
||||
if dtu.Cfg.ShowProgress {
|
||||
fmt.Fprintf(builder, "\r[%s] ↓ %s/%s %s/s(%s/s) in %s, left %s ............", dtu.taskInfo.Id(),
|
||||
downloadedPercentage := fmt.Sprintf("%.2f%%", float64(status.Downloaded())/float64(status.TotalSize())*100)
|
||||
fmt.Fprintf(builder, "\r[%s] ↓ %s/%s(%s) %s/s(%s/s) in %s, left %s ............", dtu.taskInfo.Id(),
|
||||
converter.ConvertFileSize(status.Downloaded(), 2),
|
||||
converter.ConvertFileSize(status.TotalSize(), 2),
|
||||
downloadedPercentage,
|
||||
converter.ConvertFileSize(status.SpeedsPerSecond(), 2),
|
||||
converter.ConvertFileSize(dtu.GlobalSpeedsStat.GetSpeeds(), 2),
|
||||
status.TimeElapsed()/1e7*1e7, leftStr,
|
||||
@@ -271,7 +292,7 @@ func (dtu *DownloadTaskUnit) download() (err error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
//panHTTPClient 获取包含特定User-Agent的HTTPClient
|
||||
// panHTTPClient 获取包含特定User-Agent的HTTPClient
|
||||
func (dtu *DownloadTaskUnit) panHTTPClient() (client *requester.HTTPClient) {
|
||||
client = requester.NewHTTPClient()
|
||||
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
@@ -311,7 +332,7 @@ func (dtu *DownloadTaskUnit) handleError(result *taskframework.TaskUnitRunResult
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
//checkFileValid 检测文件有效性
|
||||
// checkFileValid 检测文件有效性
|
||||
func (dtu *DownloadTaskUnit) checkFileValid(result *taskframework.TaskUnitRunResult) (ok bool) {
|
||||
if dtu.NoCheck {
|
||||
// 不检测文件有效性
|
||||
@@ -368,6 +389,20 @@ func (dtu *DownloadTaskUnit) OnRetry(lastRunResult *taskframework.TaskUnitRunRes
|
||||
func (dtu *DownloadTaskUnit) OnSuccess(lastRunResult *taskframework.TaskUnitRunResult) {
|
||||
// 执行插件
|
||||
dtu.pluginCallback("success")
|
||||
|
||||
// 下载文件数据记录
|
||||
if config.Config.FileRecordConfig == "1" {
|
||||
if dtu.fileInfo.IsFile() {
|
||||
if dtu.FileRecorder != nil {
|
||||
dtu.FileRecorder.Append(&log.FileRecordItem{
|
||||
Status: "成功",
|
||||
TimeStr: utils.NowTimeStr(),
|
||||
FileSize: dtu.fileInfo.FileSize,
|
||||
FilePath: dtu.fileInfo.Path,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (dtu *DownloadTaskUnit) OnFailed(lastRunResult *taskframework.TaskUnitRunResult) {
|
||||
@@ -422,19 +457,26 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
||||
result = &taskframework.TaskUnitRunResult{}
|
||||
// 获取文件信息
|
||||
var apierr *apierror.ApiError
|
||||
if dtu.fileInfo == nil || dtu.taskInfo.Retry() > 0 {
|
||||
// 没有获取文件信息
|
||||
// 如果是动态添加的下载任务, 是会写入文件信息的
|
||||
// 如果该任务重试过, 则应该再获取一次文件信息
|
||||
dtu.fileInfo, apierr = dtu.PanClient.FileInfoByPath(dtu.DriveId, dtu.FilePanPath)
|
||||
if apierr != nil {
|
||||
// 如果不是未登录或文件不存在, 则不重试
|
||||
result.ResultMessage = "获取下载路径信息错误"
|
||||
result.Err = apierr
|
||||
dtu.handleError(result)
|
||||
return
|
||||
if dtu.downloadFileSource != AlbumFileSource { // 相簿文件信息是传递进来的,无法在这里获取
|
||||
if dtu.fileInfo == nil || dtu.taskInfo.Retry() > 0 {
|
||||
// 没有获取文件信息
|
||||
// 如果是动态添加的下载任务, 是会写入文件信息的
|
||||
// 如果该任务重试过, 则应该再获取一次文件信息
|
||||
dtu.fileInfo, apierr = dtu.PanClient.FileInfoByPath(dtu.DriveId, dtu.FilePanPath)
|
||||
if apierr != nil {
|
||||
// 如果不是未登录或文件不存在, 则不重试
|
||||
result.ResultMessage = "获取下载路径信息错误"
|
||||
result.Err = apierr
|
||||
dtu.handleError(result)
|
||||
return
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
} else {
|
||||
if dtu.taskInfo.Retry() > 0 {
|
||||
// 延时避免风控
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
// 输出文件信息
|
||||
@@ -602,6 +644,14 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
||||
return result
|
||||
}
|
||||
|
||||
// 文件下载成功,更改文件修改时间
|
||||
if dtu.downloadFileSource == AlbumFileSource {
|
||||
// 只有相册文件才需要更改时间
|
||||
if err := os.Chtimes(dtu.SavePath, utils.ParseTimeStr(dtu.fileInfo.CreatedAt), utils.ParseTimeStr(dtu.fileInfo.CreatedAt)); err != nil {
|
||||
logger.Verbosef(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// 统计下载
|
||||
dtu.DownloadStatistic.AddTotalSize(dtu.fileInfo.FileSize)
|
||||
// 下载成功
|
||||
|
||||
@@ -16,7 +16,6 @@ package panupload
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/library-go/requester"
|
||||
"io"
|
||||
@@ -45,13 +44,6 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
uploadUrlExpired = fmt.Errorf("UrlExpired")
|
||||
uploadPartNotSeq = fmt.Errorf("PartNotSequential")
|
||||
uploadTerminate = fmt.Errorf("UploadErrorTerminate")
|
||||
uploadPartAlreadyExist = fmt.Errorf("PartAlreadyExist")
|
||||
)
|
||||
|
||||
func (e EmptyReaderLen64) Read(p []byte) (n int, err error) {
|
||||
return 0, io.EOF
|
||||
}
|
||||
@@ -104,8 +96,9 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
|
||||
}
|
||||
newUploadInfo, err := pu.panClient.GetUploadUrl(refreshUploadParam)
|
||||
if err != nil {
|
||||
logger.Verboseln(err)
|
||||
return false, &uploader.MultiError{
|
||||
Err: uploadUrlExpired,
|
||||
Err: uploader.UploadUrlExpired,
|
||||
Terminated: false,
|
||||
}
|
||||
}
|
||||
@@ -141,24 +134,24 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
|
||||
if err := xml.Unmarshal(buf, errResp); err == nil {
|
||||
if errResp.Code != "" {
|
||||
if "PartNotSequential" == errResp.Code || "NoSuchUpload" == errResp.Code {
|
||||
respError = uploadPartNotSeq
|
||||
respError = uploader.UploadPartNotSeq
|
||||
respErr = &uploader.MultiError{
|
||||
Err: uploadPartNotSeq,
|
||||
Err: uploader.UploadPartNotSeq,
|
||||
Terminated: false,
|
||||
NeedStartOver: true,
|
||||
}
|
||||
return resp, respError
|
||||
} else if "AccessDenied" == errResp.Code && "Request has expired." == errResp.Message {
|
||||
respError = uploadUrlExpired
|
||||
respError = uploader.UploadUrlExpired
|
||||
respErr = &uploader.MultiError{
|
||||
Err: uploadUrlExpired,
|
||||
Err: uploader.UploadUrlExpired,
|
||||
Terminated: false,
|
||||
}
|
||||
return resp, respError
|
||||
} else if "PartAlreadyExist" == errResp.Code {
|
||||
respError = uploadPartAlreadyExist
|
||||
respError = uploader.UploadPartAlreadyExist
|
||||
respErr = &uploader.MultiError{
|
||||
Err: uploadPartAlreadyExist,
|
||||
Err: uploader.UploadPartAlreadyExist,
|
||||
Terminated: false,
|
||||
}
|
||||
return resp, respError
|
||||
@@ -173,13 +166,13 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
|
||||
// 不可恢复的错误
|
||||
switch resp.StatusCode {
|
||||
case 400, 401, 403, 413, 600:
|
||||
respError = uploadTerminate
|
||||
respError = uploader.UploadTerminate
|
||||
respErr = &uploader.MultiError{
|
||||
Terminated: true,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
respError = uploadTerminate
|
||||
respError = uploader.UploadTerminate
|
||||
respErr = &uploader.MultiError{
|
||||
Terminated: true,
|
||||
}
|
||||
@@ -195,7 +188,7 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
|
||||
apiError := pu.panClient.UploadFileData(uploadUrl, uploadFunc)
|
||||
|
||||
if respErr != nil {
|
||||
if respErr.Err == uploadUrlExpired {
|
||||
if respErr.Err == uploader.UploadUrlExpired {
|
||||
// URL过期,获取新的URL
|
||||
guur, er := pu.panClient.GetUploadUrl(&aliyunpan.GetUploadUrlParam{
|
||||
DriveId: pu.driveId,
|
||||
@@ -216,11 +209,11 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
|
||||
uploadUrl = pu.uploadOpEntity.PartInfoList[partseq].InternalUploadURL
|
||||
}
|
||||
apiError = pu.panClient.UploadFileData(uploadUrl, uploadFunc)
|
||||
} else if respErr.Err == uploadPartAlreadyExist {
|
||||
} else if respErr.Err == uploader.UploadPartAlreadyExist {
|
||||
// already upload
|
||||
// success
|
||||
return true, nil
|
||||
} else if respErr.Err == uploadPartNotSeq {
|
||||
} else if respErr.Err == uploader.UploadPartNotSeq {
|
||||
// 上传分片乱序了,需要重新从0分片开始上传
|
||||
// 先直接返回,后续再优化
|
||||
return false, respErr
|
||||
@@ -245,6 +238,19 @@ func (pu *PanUpload) CommitFile() (cerr error) {
|
||||
FileId: pu.uploadOpEntity.FileId,
|
||||
UploadId: pu.uploadOpEntity.UploadId,
|
||||
})
|
||||
if er != nil && er.Code == apierror.ApiCodeDeviceSessionSignatureInvalid {
|
||||
_, e := pu.panClient.CreateSession(nil)
|
||||
if e == nil {
|
||||
// retry
|
||||
_, er = pu.panClient.CompleteUploadFile(&aliyunpan.CompleteUploadFileParam{
|
||||
DriveId: pu.driveId,
|
||||
FileId: pu.uploadOpEntity.FileId,
|
||||
UploadId: pu.uploadOpEntity.UploadId,
|
||||
})
|
||||
} else {
|
||||
logger.Verboseln("CreateSession failed")
|
||||
}
|
||||
}
|
||||
if er != nil {
|
||||
return er
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ package panupload
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan/internal/log"
|
||||
"github.com/tickstep/aliyunpan/internal/plugins"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"os"
|
||||
@@ -71,6 +72,9 @@ type (
|
||||
|
||||
// 全局速度统计
|
||||
GlobalSpeedsStat *speeds.Speeds
|
||||
|
||||
// 上传文件记录器
|
||||
FileRecorder *log.FileRecorder
|
||||
}
|
||||
)
|
||||
|
||||
@@ -118,11 +122,11 @@ func (utu *UploadTaskUnit) prepareFile() {
|
||||
return
|
||||
}
|
||||
|
||||
if utu.LocalFileChecksum.Length > MaxRapidUploadSize {
|
||||
fmt.Printf("[%s] 文件超过20GB, 无法使用秒传功能, 跳过秒传...\n", utu.taskInfo.Id())
|
||||
utu.Step = StepUploadUpload
|
||||
return
|
||||
}
|
||||
//if utu.LocalFileChecksum.Length > MaxRapidUploadSize {
|
||||
// fmt.Printf("[%s] 文件超过20GB, 无法使用秒传功能, 跳过秒传...\n", utu.taskInfo.Id())
|
||||
// utu.Step = StepUploadUpload
|
||||
// return
|
||||
//}
|
||||
// 下一步: 秒传
|
||||
utu.Step = StepUploadRapidUpload
|
||||
}
|
||||
@@ -175,9 +179,11 @@ func (utu *UploadTaskUnit) upload() (result *taskframework.TaskUnitRunResult) {
|
||||
}
|
||||
|
||||
if utu.ShowProgress {
|
||||
fmt.Printf("\r[%s] ↑ %s/%s %s/s(%s/s) in %s ............", utu.taskInfo.Id(),
|
||||
uploadedPercentage := fmt.Sprintf("%.2f%%", float64(status.Uploaded())/float64(status.TotalSize())*100)
|
||||
fmt.Printf("\r[%s] ↑ %s/%s(%s) %s/s(%s/s) in %s ............", utu.taskInfo.Id(),
|
||||
converter.ConvertFileSize(status.Uploaded(), 2),
|
||||
converter.ConvertFileSize(status.TotalSize(), 2),
|
||||
uploadedPercentage,
|
||||
converter.ConvertFileSize(status.SpeedsPerSecond(), 2),
|
||||
converter.ConvertFileSize(utu.GlobalSpeedsStat.GetSpeeds(), 2),
|
||||
status.TimeElapsed(),
|
||||
@@ -239,6 +245,16 @@ func (utu *UploadTaskUnit) OnRetry(lastRunResult *taskframework.TaskUnitRunResul
|
||||
func (utu *UploadTaskUnit) OnSuccess(lastRunResult *taskframework.TaskUnitRunResult) {
|
||||
// 执行插件
|
||||
utu.pluginCallback("success")
|
||||
|
||||
// 上传文件数据记录
|
||||
if config.Config.FileRecordConfig == "1" {
|
||||
utu.FileRecorder.Append(&log.FileRecordItem{
|
||||
Status: "成功",
|
||||
TimeStr: utils.NowTimeStr(),
|
||||
FileSize: utu.LocalFileChecksum.LocalFileMeta.Length,
|
||||
FilePath: utu.LocalFileChecksum.Path.LogicPath,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (utu *UploadTaskUnit) OnFailed(lastRunResult *taskframework.TaskUnitRunResult) {
|
||||
@@ -320,6 +336,7 @@ func (utu *UploadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
||||
var proofCode = ""
|
||||
var localFileInfo os.FileInfo
|
||||
var localFile *os.File
|
||||
var newBlockSize int64
|
||||
|
||||
switch utu.Step {
|
||||
case StepUploadPrepareUpload:
|
||||
@@ -335,24 +352,42 @@ 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)
|
||||
fe, apierr1 := utu.PanClient.FileInfoByPath(utu.DriveId, saveFilePath)
|
||||
time.Sleep(1 * time.Second)
|
||||
needToCreateFolder := false
|
||||
if apierr1 != nil && apierr1.Code == apierror.ApiCodeFileNotFoundCode {
|
||||
needToCreateFolder = true
|
||||
} else {
|
||||
if fe == nil {
|
||||
needToCreateFolder = true
|
||||
} else {
|
||||
rs = &aliyunpan.MkdirResult{}
|
||||
rs.FileId = fe.FileId
|
||||
}
|
||||
}
|
||||
if needToCreateFolder {
|
||||
logger.Verbosef("[%s] %s 创建云盘文件夹: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), saveFilePath)
|
||||
// rs, apierr = utu.PanClient.MkdirRecursive(utu.DriveId, "", "", 0, strings.Split(path.Clean(saveFilePath), "/"))
|
||||
// 可以直接创建的,不用循环创建
|
||||
rs, apierr = utu.PanClient.Mkdir(utu.DriveId, "root", saveFilePath)
|
||||
if apierr != nil && apierr.Code == apierror.ApiCodeDeviceSessionSignatureInvalid {
|
||||
_, e := utu.PanClient.CreateSession(nil)
|
||||
if e == nil {
|
||||
// retry
|
||||
rs, apierr = utu.PanClient.Mkdir(utu.DriveId, "root", saveFilePath)
|
||||
} else {
|
||||
logger.Verboseln("CreateSession failed")
|
||||
}
|
||||
}
|
||||
if apierr != nil || rs.FileId == "" {
|
||||
result.Err = apierr
|
||||
result.ResultMessage = "创建云盘文件夹失败"
|
||||
utu.FolderCreateMutex.Unlock()
|
||||
return
|
||||
}
|
||||
logger.Verbosef("[%s] %s 创建云盘文件夹成功\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
|
||||
} else {
|
||||
rs = &aliyunpan.MkdirResult{}
|
||||
rs.FileId = fe.FileId
|
||||
}
|
||||
utu.FolderCreateMutex.Unlock()
|
||||
} else {
|
||||
rs = &aliyunpan.MkdirResult{}
|
||||
rs.FileId = ""
|
||||
@@ -413,6 +448,13 @@ StepUploadPrepareUpload:
|
||||
}
|
||||
}
|
||||
|
||||
// 自动调整BlockSize大小
|
||||
newBlockSize = utils.ResizeUploadBlockSize(utu.LocalFileChecksum.Length, utu.BlockSize)
|
||||
if newBlockSize != utu.BlockSize {
|
||||
logger.Verboseln("resize upload block size to: " + converter.ConvertFileSize(newBlockSize, 2))
|
||||
utu.BlockSize = newBlockSize
|
||||
}
|
||||
|
||||
// 创建上传任务
|
||||
appCreateUploadFileParam = &aliyunpan.CreateFileUploadParam{
|
||||
DriveId: utu.DriveId,
|
||||
@@ -428,6 +470,15 @@ StepUploadPrepareUpload:
|
||||
}
|
||||
|
||||
uploadOpEntity, apierr = utu.PanClient.CreateUploadFile(appCreateUploadFileParam)
|
||||
if apierr != nil && apierr.Code == apierror.ApiCodeDeviceSessionSignatureInvalid {
|
||||
_, e := utu.PanClient.CreateSession(nil)
|
||||
if e == nil {
|
||||
// retry
|
||||
uploadOpEntity, apierr = utu.PanClient.CreateUploadFile(appCreateUploadFileParam)
|
||||
} else {
|
||||
logger.Verboseln("CreateSession failed")
|
||||
}
|
||||
}
|
||||
if apierr != nil {
|
||||
result.Err = apierr
|
||||
result.ResultMessage = "创建上传任务失败:" + apierr.Error()
|
||||
@@ -456,7 +507,7 @@ stepUploadUpload:
|
||||
// 正常上传流程
|
||||
uploadResult := utu.upload()
|
||||
if uploadResult != nil && uploadResult.Err != nil {
|
||||
if uploadResult.Err == uploadPartNotSeq {
|
||||
if uploadResult.Err == uploader.UploadPartNotSeq {
|
||||
fmt.Printf("[%s] %s 文件分片上传顺序错误,开始重新上传文件\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
|
||||
// 需要重新从0开始上传
|
||||
uploadResult = nil
|
||||
|
||||
@@ -55,7 +55,7 @@ func IsUrlExpired(urlStr string) bool {
|
||||
}
|
||||
expiredTimeSecStr := u.Query().Get("x-oss-expires")
|
||||
expiredTimeSec, _ := strconv.ParseInt(expiredTimeSecStr, 10, 64)
|
||||
if (time.Now().Unix() - 10) >= expiredTimeSec {
|
||||
if (expiredTimeSec - time.Now().Unix()) <= 300 { // 小于5分钟
|
||||
// expired
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type (
|
||||
FileRecordItem struct {
|
||||
Status string `json:"status"`
|
||||
TimeStr string `json:"timeStr"`
|
||||
FileSize int64 `json:"fileSize"`
|
||||
FilePath string `json:"filePath"`
|
||||
}
|
||||
|
||||
FileRecorder struct {
|
||||
Path string `json:"path"`
|
||||
locker *sync.Mutex
|
||||
}
|
||||
)
|
||||
|
||||
// NewFileRecorder 创建文件记录器
|
||||
func NewFileRecorder(filePath string) *FileRecorder {
|
||||
return &FileRecorder{
|
||||
Path: filePath,
|
||||
locker: &sync.Mutex{},
|
||||
}
|
||||
}
|
||||
|
||||
// Append 增加数据记录
|
||||
func (f *FileRecorder) Append(item *FileRecordItem) error {
|
||||
f.locker.Lock()
|
||||
defer f.locker.Unlock()
|
||||
savePath := f.Path
|
||||
folder := filepath.Dir(savePath)
|
||||
if b, err := utils.PathExists(folder); err == nil && !b {
|
||||
os.MkdirAll(folder, 0755)
|
||||
}
|
||||
|
||||
var fp *os.File
|
||||
var write *csv.Writer
|
||||
if b, err := utils.PathExists(savePath); err == nil && b {
|
||||
file, err1 := os.OpenFile(savePath, os.O_APPEND, 0755)
|
||||
if err1 != nil {
|
||||
logger.Verbosef("打开文件["+savePath+"]失败,%v", err1)
|
||||
return err1
|
||||
}
|
||||
fp = file
|
||||
write = csv.NewWriter(fp) //创建一个新的写入文件流
|
||||
} else {
|
||||
file, err1 := os.OpenFile(savePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) // 创建文件句柄
|
||||
if err1 != nil {
|
||||
logger.Verbosef("创建文件["+savePath+"]失败,%v", err1)
|
||||
return err1
|
||||
}
|
||||
fp = file
|
||||
fp.WriteString("\xEF\xBB\xBF") // 写入UTF-8 BOM
|
||||
write = csv.NewWriter(fp) //创建一个新的写入文件流
|
||||
write.Write([]string{"状态", "时间", "文件大小", "文件路径"})
|
||||
}
|
||||
if fp == nil || write == nil {
|
||||
return fmt.Errorf("open recorder file error")
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
data := []string{item.Status, item.TimeStr, converter.ConvertFileSize(item.FileSize, 2), item.FilePath}
|
||||
write.Write(data)
|
||||
write.Flush()
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package log
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCsvFile(t *testing.T) {
|
||||
savePath := "D:\\smb\\feny\\goprojects\\dev\\logs\\file_upload_records.csv"
|
||||
recorder := NewFileRecorder(savePath)
|
||||
recorder.Append(&FileRecordItem{
|
||||
Status: "成功",
|
||||
TimeStr: "2022-12-19 16:46:36",
|
||||
FileSize: 453450,
|
||||
FilePath: "D:\\smb\\feny\\goprojects\\dev\\myfile.mp4",
|
||||
})
|
||||
}
|
||||
@@ -5,12 +5,15 @@ import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/file/downloader"
|
||||
"github.com/tickstep/aliyunpan/internal/file/uploader"
|
||||
"github.com/tickstep/aliyunpan/internal/functions/panupload"
|
||||
"github.com/tickstep/aliyunpan/internal/localfile"
|
||||
"github.com/tickstep/aliyunpan/internal/log"
|
||||
"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"
|
||||
"github.com/tickstep/library-go/requester/rio"
|
||||
@@ -38,6 +41,9 @@ type (
|
||||
|
||||
localFolderCreateMutex *sync.Mutex
|
||||
panFolderCreateMutex *sync.Mutex
|
||||
|
||||
// 文件记录器,存储同步文件记录
|
||||
fileRecorder *log.FileRecorder
|
||||
}
|
||||
)
|
||||
|
||||
@@ -67,12 +73,15 @@ func (f *FileActionTask) DoAction(ctx context.Context) error {
|
||||
} else {
|
||||
// upload success, post operation
|
||||
// save local file info into db
|
||||
var actFile *aliyunpan.FileEntity
|
||||
if f.syncItem.UploadEntity != nil && f.syncItem.UploadEntity.FileId != "" {
|
||||
if file, er := f.panClient.FileInfoById(f.syncItem.DriveId, f.syncItem.UploadEntity.FileId); er == nil {
|
||||
file.Path = f.syncItem.getPanFileFullPath()
|
||||
fItem := NewPanFileItem(file)
|
||||
fItem.ScanTimeAt = utils.NowTimeStr()
|
||||
f.panFileDb.Add(fItem)
|
||||
|
||||
actFile = file
|
||||
}
|
||||
} else {
|
||||
if file, er := f.panClient.FileInfoByPath(f.syncItem.DriveId, f.syncItem.getPanFileFullPath()); er == nil {
|
||||
@@ -80,8 +89,20 @@ func (f *FileActionTask) DoAction(ctx context.Context) error {
|
||||
fItem := NewPanFileItem(file)
|
||||
fItem.ScanTimeAt = utils.NowTimeStr()
|
||||
f.panFileDb.Add(fItem)
|
||||
|
||||
actFile = file
|
||||
}
|
||||
}
|
||||
|
||||
// recorder
|
||||
if actFile != nil {
|
||||
f.appendRecord(&log.FileRecordItem{
|
||||
Status: "成功-上传",
|
||||
TimeStr: utils.NowTimeStr(),
|
||||
FileSize: actFile.FileSize,
|
||||
FilePath: actFile.Path,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,6 +157,14 @@ func (f *FileActionTask) DoAction(ctx context.Context) error {
|
||||
ScanStatus: ScanStatusNormal,
|
||||
})
|
||||
}
|
||||
|
||||
// recorder
|
||||
f.appendRecord(&log.FileRecordItem{
|
||||
Status: "成功-下载",
|
||||
TimeStr: utils.NowTimeStr(),
|
||||
FileSize: f.syncItem.PanFile.FileSize,
|
||||
FilePath: f.syncItem.PanFile.Path,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -148,6 +177,14 @@ func (f *FileActionTask) DoAction(ctx context.Context) error {
|
||||
// clear DB
|
||||
f.localFileDb.Delete(f.syncItem.getLocalFileFullPath())
|
||||
f.panFileDb.Delete(f.syncItem.getPanFileFullPath())
|
||||
|
||||
// recorder
|
||||
f.appendRecord(&log.FileRecordItem{
|
||||
Status: "成功-删除本地文件",
|
||||
TimeStr: utils.NowTimeStr(),
|
||||
FileSize: 0,
|
||||
FilePath: f.syncItem.getLocalFileFullPath(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,6 +197,14 @@ func (f *FileActionTask) DoAction(ctx context.Context) error {
|
||||
// clear DB
|
||||
f.localFileDb.Delete(f.syncItem.getLocalFileFullPath())
|
||||
f.panFileDb.Delete(f.syncItem.getPanFileFullPath())
|
||||
|
||||
// recorder
|
||||
f.appendRecord(&log.FileRecordItem{
|
||||
Status: "成功-删除云盘文件",
|
||||
TimeStr: utils.NowTimeStr(),
|
||||
FileSize: 0,
|
||||
FilePath: f.syncItem.getPanFileFullPath(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -450,6 +495,15 @@ func (f *FileActionTask) uploadFile(ctx context.Context) error {
|
||||
localFileInfo, _ := localFileEntity.Stat()
|
||||
proofCode = aliyunpan.CalcProofCode(f.panClient.GetAccessToken(), rio.NewFileReaderAtLen64(localFileEntity), localFileInfo.Size())
|
||||
|
||||
// 自动调整BlockSize大小
|
||||
newBlockSize := utils.ResizeUploadBlockSize(localFile.Length, f.syncItem.UploadBlockSize)
|
||||
if newBlockSize != f.syncItem.UploadBlockSize {
|
||||
logger.Verboseln("resize upload block size to: " + converter.ConvertFileSize(newBlockSize, 2))
|
||||
f.syncItem.UploadBlockSize = newBlockSize
|
||||
// 存储状态
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
}
|
||||
|
||||
// 创建上传任务
|
||||
appCreateUploadFileParam := &aliyunpan.CreateFileUploadParam{
|
||||
DriveId: f.syncItem.DriveId,
|
||||
@@ -706,3 +760,13 @@ func (f *FileActionTask) createPanFolder(ctx context.Context) error {
|
||||
return apierr1
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FileActionTask) appendRecord(item *log.FileRecordItem) error {
|
||||
if item == nil {
|
||||
return nil
|
||||
}
|
||||
if config.Config.FileRecordConfig == "1" {
|
||||
return f.fileRecorder.Append(item)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -729,6 +729,7 @@ func (f *FileActionTaskManager) getFromSyncDb(act SyncFileAction) *FileActionTas
|
||||
maxUploadRate: f.syncOption.MaxUploadRate,
|
||||
localFolderCreateMutex: f.localCreateMutex,
|
||||
panFolderCreateMutex: f.folderCreateMutex,
|
||||
fileRecorder: f.syncOption.FileRecorder,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -747,6 +748,7 @@ func (f *FileActionTaskManager) getFromSyncDb(act SyncFileAction) *FileActionTas
|
||||
maxUploadRate: f.syncOption.MaxUploadRate,
|
||||
localFolderCreateMutex: f.localCreateMutex,
|
||||
panFolderCreateMutex: f.folderCreateMutex,
|
||||
fileRecorder: f.syncOption.FileRecorder,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -767,6 +769,7 @@ func (f *FileActionTaskManager) getFromSyncDb(act SyncFileAction) *FileActionTas
|
||||
maxUploadRate: f.syncOption.MaxUploadRate,
|
||||
localFolderCreateMutex: f.localCreateMutex,
|
||||
panFolderCreateMutex: f.folderCreateMutex,
|
||||
fileRecorder: f.syncOption.FileRecorder,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,16 @@ func TestFileActionMgrStart(t *testing.T) {
|
||||
}
|
||||
|
||||
// pan client
|
||||
panClient := aliyunpan.NewPanClient(*webToken, aliyunpan.AppLoginToken{})
|
||||
panClient := aliyunpan.NewPanClient(*webToken, aliyunpan.AppLoginToken{}, aliyunpan.AppConfig{
|
||||
AppId: "25dzX3vbYqktVxyX",
|
||||
DeviceId: "E75459EXhOTkI5ZI6S3qDHA3",
|
||||
UserId: "",
|
||||
Nonce: 0,
|
||||
PublicKey: "",
|
||||
}, aliyunpan.SessionConfig{
|
||||
DeviceName: "Chrome浏览器",
|
||||
ModelName: "Windows网页版",
|
||||
})
|
||||
user, _ := panClient.GetUserInfo()
|
||||
task := &SyncTask{
|
||||
Id: "5b2d7c10-e927-4e72-8f9d-5abb3bb04814",
|
||||
|
||||
@@ -25,7 +25,16 @@ func TestPanSyncDb(t *testing.T) {
|
||||
}
|
||||
|
||||
// pan client
|
||||
panClient := aliyunpan.NewPanClient(*webToken, aliyunpan.AppLoginToken{})
|
||||
panClient := aliyunpan.NewPanClient(*webToken, aliyunpan.AppLoginToken{}, aliyunpan.AppConfig{
|
||||
AppId: "25dzX3vbYqktVxyX",
|
||||
DeviceId: "E75459EXhOTkI5ZI6S3qDHA3",
|
||||
UserId: "",
|
||||
Nonce: 0,
|
||||
PublicKey: "",
|
||||
}, aliyunpan.SessionConfig{
|
||||
DeviceName: "Chrome浏览器",
|
||||
ModelName: "Windows网页版",
|
||||
})
|
||||
|
||||
// get user info
|
||||
ui, err := panClient.GetUserInfo()
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/log"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"io/ioutil"
|
||||
@@ -29,6 +30,9 @@ type (
|
||||
|
||||
// 本地文件修改检测间隔
|
||||
LocalFileModifiedCheckIntervalSec int
|
||||
|
||||
// 文件记录器
|
||||
FileRecorder *log.FileRecorder
|
||||
}
|
||||
|
||||
// SyncTaskManager 同步任务管理器
|
||||
|
||||
@@ -16,7 +16,16 @@ func TestStart(t *testing.T) {
|
||||
}
|
||||
|
||||
// pan client
|
||||
panClient := aliyunpan.NewPanClient(*webToken, aliyunpan.AppLoginToken{})
|
||||
panClient := aliyunpan.NewPanClient(*webToken, aliyunpan.AppLoginToken{}, aliyunpan.AppConfig{
|
||||
AppId: "25dzX3vbYqktVxyX",
|
||||
DeviceId: "E75459EXhOTkI5ZI6S3qDHA3",
|
||||
UserId: "",
|
||||
Nonce: 0,
|
||||
PublicKey: "",
|
||||
}, aliyunpan.SessionConfig{
|
||||
DeviceName: "Chrome浏览器",
|
||||
ModelName: "Windows网页版",
|
||||
})
|
||||
user, _ := panClient.GetUserInfo()
|
||||
|
||||
manager := NewSyncTaskManager(
|
||||
@@ -27,7 +36,7 @@ func TestStart(t *testing.T) {
|
||||
SyncOption{},
|
||||
)
|
||||
|
||||
manager.Start(nil)
|
||||
manager.Start(nil, StepSyncFile)
|
||||
time.Sleep(30 * time.Minute)
|
||||
manager.Stop()
|
||||
manager.Stop(StepSyncFile)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,16 @@ func TestSyncTask(t *testing.T) {
|
||||
}
|
||||
|
||||
// pan client
|
||||
panClient := aliyunpan.NewPanClient(*webToken, aliyunpan.AppLoginToken{})
|
||||
panClient := aliyunpan.NewPanClient(*webToken, aliyunpan.AppLoginToken{}, aliyunpan.AppConfig{
|
||||
AppId: "25dzX3vbYqktVxyX",
|
||||
DeviceId: "E75459EXhOTkI5ZI6S3qDHA3",
|
||||
UserId: "",
|
||||
Nonce: 0,
|
||||
PublicKey: "",
|
||||
}, aliyunpan.SessionConfig{
|
||||
DeviceName: "Chrome浏览器",
|
||||
ModelName: "Windows网页版",
|
||||
})
|
||||
user, _ := panClient.GetUserInfo()
|
||||
task := SyncTask{
|
||||
Id: "5b2d7c10-e927-4e72-8f9d-5abb3bb04814",
|
||||
|
||||
@@ -20,9 +20,11 @@ import (
|
||||
"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/cookiejar"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -270,3 +272,12 @@ func IsExcludeFile(filePath string, excludeNames *[]string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// ResizeUploadBlockSize 自动调整分片大小,方便支持极大单文件上传。返回新的分片大小
|
||||
func ResizeUploadBlockSize(fileSize, defaultBlockSize int64) int64 {
|
||||
if (aliyunpan.MaxPartNum * defaultBlockSize) > fileSize {
|
||||
return defaultBlockSize
|
||||
}
|
||||
sizeOfMB := int64(math.Ceil(float64(fileSize) / float64(aliyunpan.MaxPartNum) / 1024.0))
|
||||
return sizeOfMB * 1024
|
||||
}
|
||||
|
||||
@@ -39,3 +39,15 @@ func TestIsAbsPath_ReturnTrue(t *testing.T) {
|
||||
func TestIsAbsPath_ReturnFalse(t *testing.T) {
|
||||
fmt.Println(IsLocalAbsPath("my\\folder\\test"))
|
||||
}
|
||||
|
||||
func TestResizeUploadBlockSize_ReturnDefaultBlockSize(t *testing.T) {
|
||||
MB := int64(1024 * 1024) // 1048576
|
||||
fileSize := int64(1073741824) // 90GB
|
||||
fmt.Println(ResizeUploadBlockSize(fileSize, 10*MB)) // 10485760 = 10240KB
|
||||
}
|
||||
|
||||
func TestResizeUploadBlockSize_ReturnNewBlockSize(t *testing.T) {
|
||||
MB := int64(1024 * 1024) // 1048576
|
||||
fileSize := int64(107374182400) // 100GB
|
||||
fmt.Println(ResizeUploadBlockSize(fileSize, 10*MB)) // 10737664 = 10486KB
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package filelocker
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrTimeout = errors.New("timeout")
|
||||
)
|
||||
@@ -0,0 +1,25 @@
|
||||
package filelocker
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
lockExt = ".lock"
|
||||
)
|
||||
|
||||
type (
|
||||
FileLocker struct {
|
||||
FilePath string
|
||||
LockFilePath string
|
||||
lockFile *os.File
|
||||
}
|
||||
)
|
||||
|
||||
func NewFileLocker(path string) *FileLocker {
|
||||
return &FileLocker{
|
||||
FilePath: path,
|
||||
LockFilePath: path + lockExt,
|
||||
lockFile: nil,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package filelocker
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestFlocker(t *testing.T) {
|
||||
// lock file first time - success
|
||||
locker := NewFileLocker("D:\\smb\\feny\\goprojects\\dev\\aliyunpan")
|
||||
e := LockFile(locker, 0755, true, 5*time.Second)
|
||||
fmt.Println(e)
|
||||
|
||||
// lock file again - fail
|
||||
//time.Sleep(5 * time.Second)
|
||||
//e = flock(locker, 0755, true, 5*time.Second)
|
||||
//fmt.Println(e)
|
||||
|
||||
// Unlock the file.
|
||||
if err := UnlockFile(locker); err != nil {
|
||||
log.Printf("funlock error: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package filelocker
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LockFile acquires an advisory lock on a file descriptor.
|
||||
func LockFile(locker *FileLocker, mode os.FileMode, exclusive bool, timeout time.Duration) error {
|
||||
f, err := os.OpenFile(locker.LockFilePath, os.O_CREATE, mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
locker.lockFile = f
|
||||
|
||||
var t time.Time
|
||||
for {
|
||||
// If we're beyond our timeout then return an error.
|
||||
// This can only occur after we've attempted a flock once.
|
||||
if t.IsZero() {
|
||||
t = time.Now()
|
||||
} else if timeout > 0 && time.Since(t) > timeout {
|
||||
return ErrTimeout
|
||||
}
|
||||
var lock syscall.Flock_t
|
||||
lock.Start = 0
|
||||
lock.Len = 0
|
||||
lock.Pid = 0
|
||||
lock.Whence = 0
|
||||
lock.Pid = 0
|
||||
if exclusive {
|
||||
lock.Type = syscall.F_WRLCK
|
||||
} else {
|
||||
lock.Type = syscall.F_RDLCK
|
||||
}
|
||||
err := syscall.FcntlFlock(locker.lockFile.Fd(), syscall.F_SETLK, &lock)
|
||||
if err == nil {
|
||||
return nil
|
||||
} else if err != syscall.EAGAIN {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait for a bit and try again.
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// UnlockFile releases an advisory lock on a file descriptor.
|
||||
func UnlockFile(locker *FileLocker) error {
|
||||
var lock syscall.Flock_t
|
||||
lock.Start = 0
|
||||
lock.Len = 0
|
||||
lock.Type = syscall.F_UNLCK
|
||||
lock.Whence = 0
|
||||
return syscall.FcntlFlock(uintptr(locker.lockFile.Fd()), syscall.F_SETLK, &lock)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
//go:build !windows && !plan9 && !solaris
|
||||
// +build !windows,!plan9,!solaris
|
||||
|
||||
package filelocker
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LockFile acquires an advisory lock on a file descriptor.
|
||||
func LockFile(locker *FileLocker, mode os.FileMode, exclusive bool, timeout time.Duration) error {
|
||||
f, err := os.OpenFile(locker.LockFilePath, os.O_CREATE, mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
locker.lockFile = f
|
||||
|
||||
var t time.Time
|
||||
for {
|
||||
// If we're beyond our timeout then return an error.
|
||||
// This can only occur after we've attempted a flock once.
|
||||
if t.IsZero() {
|
||||
t = time.Now()
|
||||
} else if timeout > 0 && time.Since(t) > timeout {
|
||||
return ErrTimeout
|
||||
}
|
||||
flag := syscall.LOCK_SH
|
||||
if exclusive {
|
||||
flag = syscall.LOCK_EX
|
||||
}
|
||||
|
||||
// Otherwise attempt to obtain an exclusive lock.
|
||||
err := syscall.Flock(int(locker.lockFile.Fd()), flag|syscall.LOCK_NB)
|
||||
if err == nil {
|
||||
return nil
|
||||
} else if err != syscall.EWOULDBLOCK {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait for a bit and try again.
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// UnlockFile releases an advisory lock on a file descriptor.
|
||||
func UnlockFile(locker *FileLocker) error {
|
||||
return syscall.Flock(int(locker.lockFile.Fd()), syscall.LOCK_UN)
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package filelocker
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// LockFileEx code derived from golang build filemutex_windows.go @ v1.5.1
|
||||
var (
|
||||
modkernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
procLockFileEx = modkernel32.NewProc("LockFileEx")
|
||||
procUnlockFileEx = modkernel32.NewProc("UnlockFileEx")
|
||||
)
|
||||
|
||||
const (
|
||||
// see https://msdn.microsoft.com/en-us/library/windows/desktop/aa365203(v=vs.85).aspx
|
||||
flagLockExclusive = 2
|
||||
flagLockFailImmediately = 1
|
||||
|
||||
// see https://msdn.microsoft.com/en-us/library/windows/desktop/ms681382(v=vs.85).aspx
|
||||
errLockViolation syscall.Errno = 0x21
|
||||
)
|
||||
|
||||
func lockFileEx(h syscall.Handle, flags, reserved, locklow, lockhigh uint32, ol *syscall.Overlapped) (err error) {
|
||||
r, _, err := procLockFileEx.Call(uintptr(h), uintptr(flags), uintptr(reserved), uintptr(locklow), uintptr(lockhigh), uintptr(unsafe.Pointer(ol)))
|
||||
if r == 0 {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unlockFileEx(h syscall.Handle, reserved, locklow, lockhigh uint32, ol *syscall.Overlapped) (err error) {
|
||||
r, _, err := procUnlockFileEx.Call(uintptr(h), uintptr(reserved), uintptr(locklow), uintptr(lockhigh), uintptr(unsafe.Pointer(ol)), 0)
|
||||
if r == 0 {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// LockFile acquires an advisory lock on a file descriptor.
|
||||
func LockFile(locker *FileLocker, mode os.FileMode, exclusive bool, timeout time.Duration) error {
|
||||
// Create a separate lock file on windows because a process
|
||||
// cannot share an exclusive lock on the same file. This is
|
||||
// needed during Tx.WriteTo().
|
||||
f, err := os.OpenFile(locker.LockFilePath, os.O_CREATE, mode)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
locker.lockFile = f
|
||||
|
||||
var t time.Time
|
||||
for {
|
||||
// If we're beyond our timeout then return an error.
|
||||
// This can only occur after we've attempted a flock once.
|
||||
if t.IsZero() {
|
||||
t = time.Now()
|
||||
} else if timeout > 0 && time.Since(t) > timeout {
|
||||
return ErrTimeout
|
||||
}
|
||||
|
||||
var flag uint32 = flagLockFailImmediately
|
||||
if exclusive {
|
||||
flag |= flagLockExclusive
|
||||
}
|
||||
|
||||
err := lockFileEx(syscall.Handle(locker.lockFile.Fd()), flag, 0, 1, 0, &syscall.Overlapped{})
|
||||
if err == nil {
|
||||
return nil
|
||||
} else if err != errLockViolation {
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait for a bit and try again.
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
// UnlockFile releases an advisory lock on a file descriptor.
|
||||
func UnlockFile(locker *FileLocker) error {
|
||||
err := unlockFileEx(syscall.Handle(locker.lockFile.Fd()), 0, 1, 0, &syscall.Overlapped{})
|
||||
locker.lockFile.Close()
|
||||
os.Remove(locker.LockFilePath)
|
||||
return err
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
// 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
|
||||
// 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,
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
@@ -50,7 +51,7 @@ const (
|
||||
|
||||
var (
|
||||
// Version 版本号
|
||||
Version = "v0.2.4"
|
||||
Version = "v0.2.8"
|
||||
|
||||
// 命令历史文件
|
||||
historyFilePath = filepath.Join(config.GetConfigDir(), "aliyunpan_command_history.txt")
|
||||
@@ -68,23 +69,23 @@ func init() {
|
||||
case nil:
|
||||
case config.ErrConfigFileNoPermission, config.ErrConfigContentsParseError:
|
||||
fmt.Fprintf(os.Stderr, "FATAL ERROR: config file error: %s\n", err)
|
||||
os.Exit(1)
|
||||
//os.Exit(1)
|
||||
default:
|
||||
fmt.Printf("WARNING: config init error: %s\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func checkLoginExpiredAndRelogin() {
|
||||
cmder.ReloadConfigFunc(nil)
|
||||
command.ReloadConfigFunc(nil)
|
||||
activeUser := config.Config.ActiveUser()
|
||||
if activeUser == nil || activeUser.UserId == "" {
|
||||
// maybe expired, try to login
|
||||
cmder.TryLogin()
|
||||
command.TryLogin()
|
||||
} else {
|
||||
// 刷新过期Token并保存到配置文件
|
||||
command.RefreshTokenInNeed(activeUser)
|
||||
command.RefreshTokenInNeed(activeUser, config.Config.DeviceName)
|
||||
}
|
||||
cmder.SaveConfigFunc(nil)
|
||||
command.SaveConfigFunc(nil)
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -109,10 +110,9 @@ func main() {
|
||||
app.Name = "aliyunpan"
|
||||
app.Version = Version
|
||||
app.Author = "tickstep/aliyunpan: https://github.com/tickstep/aliyunpan"
|
||||
app.Copyright = "(c) 2021-2022 tickstep."
|
||||
app.Copyright = "(c) 2021-2023 tickstep."
|
||||
app.Usage = "阿里云盘客户端 for " + runtime.GOOS + "/" + runtime.GOARCH
|
||||
app.Description = `aliyunpan 是一款阿里云盘命令行客户端工具, 为操作阿里云盘, 提供实用功能。
|
||||
支持webdav文件协议,可以让阿里云盘变身为webdav协议的文件服务器,用于挂载作为Windows、Linux、Mac系统的磁盘进行使用。
|
||||
支持同步备份功能,支持备份本地文件到云盘,备份云盘文件到本地,双向同步备份。
|
||||
具体功能, 参见 COMMANDS 列表。
|
||||
|
||||
@@ -220,6 +220,10 @@ func main() {
|
||||
targetPath string
|
||||
)
|
||||
|
||||
if activeUser == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !closed {
|
||||
targetPath = lineArgs[numArgs-1]
|
||||
escaper.EscapeStringsByRuneFunc(lineArgs[:numArgs-1], runeFunc) // 转义
|
||||
@@ -248,6 +252,8 @@ func main() {
|
||||
wd := "/"
|
||||
if activeUser.IsFileDriveActive() {
|
||||
wd = activeUser.Workdir
|
||||
} else if activeUser.IsResourceDriveActive() {
|
||||
wd = activeUser.ResourceWorkdir
|
||||
} else if activeUser.IsAlbumDriveActive() {
|
||||
wd = activeUser.AlbumWorkdir
|
||||
}
|
||||
@@ -301,7 +307,7 @@ func main() {
|
||||
fmt.Printf("提示: 输入 help 获取帮助.\n")
|
||||
|
||||
// check update
|
||||
cmder.ReloadConfigFunc(c)
|
||||
command.ReloadConfigFunc(c)
|
||||
if config.Config.UpdateCheckInfo.LatestVer != "" {
|
||||
if utils.ParseVersionNum(config.Config.UpdateCheckInfo.LatestVer) > utils.ParseVersionNum(config.AppVersion) {
|
||||
fmt.Printf("\n当前的软件版本为:%s, 现在有新版本 %s 可供更新,强烈推荐进行更新!(可以输入 update 命令进行更新)\n\n",
|
||||
@@ -322,7 +328,7 @@ func main() {
|
||||
config.Config.UpdateCheckInfo.CheckTime = nowTime
|
||||
|
||||
// save
|
||||
cmder.SaveConfigFunc(c)
|
||||
command.SaveConfigFunc(c)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -333,7 +339,7 @@ func main() {
|
||||
)
|
||||
|
||||
if activeUser == nil {
|
||||
activeUser = cmder.TryLogin()
|
||||
activeUser = command.TryLogin()
|
||||
}
|
||||
|
||||
if activeUser != nil && activeUser.Nickname != "" {
|
||||
@@ -342,7 +348,10 @@ func main() {
|
||||
wd := "/"
|
||||
if activeUser.IsFileDriveActive() {
|
||||
wd = activeUser.Workdir
|
||||
prompt = app.Name + ":" + converter.ShortDisplay(path.Base(wd), NameShortDisplayNum) + " " + activeUser.Nickname + "$ "
|
||||
prompt = app.Name + ":" + converter.ShortDisplay(path.Base(wd), NameShortDisplayNum) + " " + activeUser.Nickname + "(备份盘)$ "
|
||||
} else if activeUser.IsResourceDriveActive() {
|
||||
wd = activeUser.ResourceWorkdir
|
||||
prompt = app.Name + ":" + converter.ShortDisplay(path.Base(wd), NameShortDisplayNum) + " " + activeUser.Nickname + "(资源库)$ "
|
||||
} else if activeUser.IsAlbumDriveActive() {
|
||||
wd = activeUser.AlbumWorkdir
|
||||
prompt = app.Name + ":" + converter.ShortDisplay(path.Base(wd), NameShortDisplayNum) + " " + activeUser.Nickname + "(相册)$ "
|
||||
@@ -429,6 +438,9 @@ func main() {
|
||||
//// 拷贝文件/目录 cp
|
||||
//command.CmdCp(),
|
||||
|
||||
// 备份盘和资源库之间拷贝文件 xcp
|
||||
command.CmdXcp(),
|
||||
|
||||
// 移动文件/目录 mv
|
||||
command.CmdMv(),
|
||||
|
||||
@@ -451,7 +463,7 @@ func main() {
|
||||
command.CmdDownload(),
|
||||
|
||||
// 获取文件下载链接
|
||||
command.CmdLocateUrl(),
|
||||
//command.CmdLocateUrl(),
|
||||
|
||||
// 导出文件/目录元数据 export
|
||||
//command.CmdExport(),
|
||||
@@ -459,8 +471,8 @@ func main() {
|
||||
// 导入文件 import
|
||||
//command.CmdImport(),
|
||||
|
||||
// webdav服务
|
||||
command.CmdWebdav(),
|
||||
// webdav服务(depressed)
|
||||
//command.CmdWebdav(),
|
||||
|
||||
// 回收站
|
||||
command.CmdRecycle(),
|
||||
@@ -596,6 +608,29 @@ func main() {
|
||||
HideHelp: true,
|
||||
},
|
||||
|
||||
// 执行系统命令
|
||||
{
|
||||
Name: "run",
|
||||
Usage: "执行系统命令",
|
||||
Category: "其他",
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() == 0 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd := exec.Command(c.Args().First(), c.Args().Tail()...)
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stderr = os.Stderr
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
|
||||
// 调试用 debug
|
||||
//{
|
||||
// Name: "debug",
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+5
-5
@@ -3,13 +3,13 @@
|
||||
"FileVersion": {
|
||||
"Major": 0,
|
||||
"Minor": 2,
|
||||
"Patch": 4,
|
||||
"Patch": 8,
|
||||
"Build": 0
|
||||
},
|
||||
"ProductVersion": {
|
||||
"Major": 0,
|
||||
"Minor": 2,
|
||||
"Patch": 4,
|
||||
"Patch": 8,
|
||||
"Build": 0
|
||||
},
|
||||
"FileFlagsMask": "3f",
|
||||
@@ -22,14 +22,14 @@
|
||||
"Comments": "",
|
||||
"CompanyName": "tickstep",
|
||||
"FileDescription": "阿里云盘客户端",
|
||||
"FileVersion": "v0.2.4",
|
||||
"FileVersion": "v0.2.8",
|
||||
"InternalName": "",
|
||||
"LegalCopyright": "© 2021-2022 tickstep.",
|
||||
"LegalCopyright": "© 2021-2023 tickstep.",
|
||||
"LegalTrademarks": "",
|
||||
"OriginalFilename": "",
|
||||
"PrivateBuild": "",
|
||||
"ProductName": "aliyunpan",
|
||||
"ProductVersion": "v0.2.4",
|
||||
"ProductVersion": "v0.2.8",
|
||||
"SpecialBuild": ""
|
||||
},
|
||||
"VarFileInfo": {
|
||||
|
||||
Reference in New Issue
Block a user