Compare commits

..

58 Commits

Author SHA1 Message Date
tickstep c81855f9c2 update to version v0.2.9 2024-01-30 16:31:50 +08:00
tickstep 65b7f28aa6 update api version to v0.1.8 2024-01-30 15:08:26 +08:00
xiaoyaofenfen 8afdb8f701 Merge pull request #381 from Sowevo/main
增加忽略SHA1跳过同名文件的功能
2024-01-30 14:51:36 +08:00
tickstep ead6ecc218 remove device offline action 2024-01-30 14:32:43 +08:00
tickstep a42f14f013 update copyright info 2024-01-30 14:13:23 +08:00
tickstep 54fff27fbe add download 100MB limit prompt for user 2024-01-30 14:10:43 +08:00
tickstep b0a1ea8591 use new api for recycle bin clear action 2024-01-30 12:01:41 +08:00
sowevo 1b78dff61e add skip args 2024-01-26 15:10:27 +08:00
sowevo f838ebfe24 add skip args 2024-01-26 15:01:27 +08:00
sowevo 7f5e19812e add skip args 2024-01-26 11:40:04 +08:00
xiaoyaofenfen 6a7da9f180 update readme file 2023-10-11 11:05:51 +08:00
xiaoyaofenfen 06184249d0 add prefer ip type config 2023-10-09 16:09:34 +08:00
xiaoyaofenfen b983d4c335 add usage demo for drive select 2023-10-09 09:33:18 +08:00
tickstep d061a28559 update version to v0.2.8 2023-10-09 01:17:32 +08:00
tickstep e91e66aadf add loong64 build script 2023-10-08 23:09:36 +08:00
tickstep 71f55424d0 update bolt version to v1.3.4 2023-10-08 23:08:49 +08:00
tickstep c6c4d871ef update aliyunpan-api to v0.1.7 2023-10-08 20:01:05 +08:00
tickstep 50e6200607 update document & readme file 2023-10-08 19:41:27 +08:00
xiaoyaofenfen c363d90e72 rename album usage 2023-10-08 17:22:36 +08:00
xiaoyaofenfen 048a6a5438 no limit the upload parallels threads #343 2023-10-08 16:55:20 +08:00
xiaoyaofenfen 19afa16bfe add album download command 2023-10-08 16:41:55 +08:00
xiaoyaofenfen b559f41e47 remove locate command 2023-10-08 14:56:47 +08:00
xiaoyaofenfen 33a5484e4a modify drive list usage 2023-10-08 14:54:59 +08:00
xiaoyaofenfen 34254bf822 format used disk quota 2023-10-08 14:23:58 +08:00
xiaoyaofenfen 66540c9457 add xcp command manual 2023-10-08 14:19:32 +08:00
xiaoyaofenfen c522b530d7 add xcp command 2023-10-08 14:12:18 +08:00
xiaoyaofenfen 3d130949c0 add share link create check 2023-10-08 09:43:27 +08:00
tickstep 1365471639 remove webdav command 2023-10-07 19:26:27 +08:00
xiaoyaofenfen 151574e154 support fast share link create 2023-10-07 17:20:16 +08:00
xiaoyaofenfen 55e989d821 adapt resource drive 2023-10-07 16:42:43 +08:00
tickstep 0edf7d0af2 label forbidden status for share list 2023-04-30 10:01:32 +08:00
tickstep ccc285ce5e add brew usage in readme file 2023-04-06 21:46:48 +08:00
tickstep 9992ffb3bc not exit app when config file read error 2023-04-02 22:23:55 +08:00
tickstep 25d916b744 update version to v0.2.7 2023-04-02 10:30:29 +08:00
tickstep 471ee93417 update version to v0.1.6 2023-04-02 10:02:14 +08:00
tickstep 78e290c2a4 optimize device session signature error #257 2023-04-02 09:58:20 +08:00
tickstep 23fe54ce0b refactor create session 2023-04-02 09:37:42 +08:00
tickstep 8d2c40f1ce fix too many request error for tree cmd 2023-04-02 08:53:59 +08:00
tickstep fa144a45a1 update aliyunpan-api to v0.1.5 2023-04-01 23:08:50 +08:00
tickstep 2740a15cbf fix device session signature error for download 2023-04-01 23:00:35 +08:00
tickstep 3be79d1181 fix device session signature error for upload #257 2023-04-01 22:48:39 +08:00
tickstep ab215736a9 refactor create session 2023-04-01 22:17:17 +08:00
tickstep 45877a4408 fix make dir faild issue #276 2023-04-01 20:30:29 +08:00
tickstep 50a05a6bec add run command for test 2023-04-01 20:20:32 +08:00
tickstep e33d23831a add winget usage to readme file 2023-03-25 08:34:44 +08:00
xiaoyaofenfen cbf372bde0 fix file info query error #232 2023-02-23 15:43:51 +08:00
xiaoyaofenfen 8d14617397 fix create folder error for upload command #244 2023-02-23 15:27:27 +08:00
xiaoyaofenfen 12d866b28a add file size percentage for upload & download command #236 2023-02-23 15:12:35 +08:00
xiaoyaofenfen 85d79a9657 add file size filter & file size showing for tree command #236 2023-02-23 14:43:02 +08:00
xiaoyaofenfen 828c1fe39a add device logout #249 2023-02-23 13:50:55 +08:00
tickstep c5a5cb906d fix signature update error 2023-02-19 17:36:23 +08:00
tickstep 4ba61c19c8 add device id config for docker 2023-02-19 17:04:45 +08:00
tickstep 571baa1482 update version to v0.2.6 2023-02-16 22:54:40 +08:00
tickstep 4575dfdaa8 add deregister device guide in readme file 2023-02-16 22:40:02 +08:00
tickstep efbf0b7bdb add deregister device guide in readme file 2023-02-16 22:39:06 +08:00
tickstep 416c5cd57e fix signature error 2023-02-16 22:25:09 +08:00
xiaoyaofenfen e0e8bc3284 add skip confirm flag for rename command 2023-01-03 11:12:26 +08:00
xiaoyaofenfen 5b22c4400a fix that the folder will created repeatly while uploading file 2023-01-03 10:57:08 +08:00
50 changed files with 1270 additions and 503 deletions
+84 -43
View File
@@ -1,40 +1,48 @@
# 关于
阿里云盘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插件),你可以按照自己的需要定制上传/下载中关键步骤的行为,最大程度满足自己的个性化需求
10. 支持个人相册的相关操作,支持批量下载相册所有照片、视频文件到本地
# 目录
- [关于](#关于)
- [特色](#特色)
- [目录](#目录)
- [如何安装](#如何安装)
* [直接下载安装](#直接下载安装)
* [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 +61,9 @@
参考例子:
```shell
wget https://github.com/tickstep/aliyunpan/releases/download/v0.2.5/aliyunpan-v0.2.5-linux-amd64.zip
unzip aliyunpan-v0.2.5-linux-amd64.zip
cd aliyunpan-v0.2.5-linux-amd64
wget https://github.com/tickstep/aliyunpan/releases/download/v0.2.9/aliyunpan-v0.2.9-linux-amd64.zip
unzip aliyunpan-v0.2.9-linux-amd64.zip
cd aliyunpan-v0.2.9-linux-amd64
./aliyunpan
```
@@ -73,6 +81,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 +114,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.5
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.9
<your local dir>:本地目录绝对路径,例如:/tickstep/Documents/设计文档
@@ -92,21 +125,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.5
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 +182,6 @@ aliyunpan > help
share 分享文件/目录
sync 同步备份功能
upload, u 上传文件/目录
webdav 在线网盘服务
...
```
@@ -178,6 +195,20 @@ aliyunpan > login -RefreshToken=32994cd2c43...4d505fa79
aliyunpan:/ tickstep$
```
### 切换网盘
支持切换备份盘、资源库,默认工作在备份盘下。
```shell
aliyunpan:/ tickstep(备份盘)$ drive
# DRIVE ID 网盘名称
1 11519221 备份盘
2 1311893110 资源库
3 61104421 相册
输入要切换的网盘 # 值 > 2
切换到网盘:资源库
aliyunpan:/ tickstep(资源库)$
```
### 查看文件列表
```shell
aliyunpan:/ tickstep$ ls
@@ -307,6 +338,15 @@ Windows
打开aliyunpan命令行程序,任何云盘命令都有类似如下日志输出
![](./assets/images/debug-log-screenshot.png)
## 如何登出和下线客户端
阿里云盘单账户最多只允许同时登录 10 台设备
当出现这个提示:你账号已超出最大登录设备数量,请先下线一台设备,然后重启本应用,才可以继续使用
说明你的账号登录客户端已经超过数量,你需要先登出其他客户端才能继续使用,如下所示
APP手机端
![](./assets/images/app-deregister-device.png)
Web网页端
![](./assets/images/web-deregister-device.png)
# 交流反馈
提交issue: [issues页面](https://github.com/tickstep/aliyunpan/issues)
联系邮箱: tickstep@outlook.com
@@ -314,4 +354,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 -1
View File
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity version="0.2.5.0" processorArchitecture="*" name="com.tickstep.aliyunpan" type="win32"/>
<assemblyIdentity version="0.2.9.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

+3
View File
@@ -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
View File
@@ -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
+9
View File
@@ -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 ]
+2
View File
@@ -37,3 +37,5 @@ services:
- ALIYUNPAN_LOCAL_DELAY_TIME=3
# 任务步骤, 支持两种: scan(只扫描并建立同步数据库),sync(正常启动同步任务)
- ALIYUNPAN_TASK_STEP=sync
# 登录设备的ID,默认为空,程序会自动生成一个ID
- ALIYUNPAN_DEVICE_ID=
+1
View File
@@ -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
+9
View File
@@ -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 ]
+2 -1
View File
@@ -25,7 +25,8 @@ services:
- ALIYUNPAN_TRANSFER_URL_TYPE=1
# 上传数据块大小,单位为KB,默认为10240KB,建议范围1024KB~10240KB
- ALIYUNPAN_BLOCK_SIZE=10240
# 登录设备的ID,默认为空,程序会自动生成一个ID
- ALIYUNPAN_DEVICE_ID=
+24 -232
View File
@@ -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-问题)
# 命令列表及说明
## 注意
@@ -249,6 +239,7 @@ aliyunpan d <网盘文件或目录的路径1> <文件或目录2> <文件或目
### 可选参数
```
--ow overwrite, 覆盖已存在的文件
--skip skip same name, 跳过已存在的同名文件,即使文件内容不一致(不检查SHA1)
--status 输出所有线程的工作状态
--save 将下载的文件直接保存到当前工作目录
--saveto value 将下载的文件直接保存到指定的目录
@@ -282,25 +273,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 +372,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 +404,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 +841,6 @@ function syncScanPanFilePrepareCallback(context, params) {
}
```
## webdav文件服务
本文命令可以让阿里云盘变身为webdav协议的文件服务器。这样你可以把阿里云盘挂载为Windows、Linux、Mac系统的磁盘,可以通过NAS系统做文件管理或文件同步等等。
当把阿里云盘作为webdav文件服务器进行使用的时候,上传文件是不支持秒传的,所以当你挂载为网络磁盘使用的时候,不建议在webdav挂载目录中上传、下载过大的文件,不然体验会非常差。
建议作为文档,图片等小文件的同步网盘。
效果图如下所示:
![](../assets/images/webdav-screenshot.png)
### 常用命令说明
```
查看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插件,你可以按照自己的需要定制上传/下载中关键步骤的行为,最大程度满足自己的个性化需求。
例如:
@@ -1447,15 +1250,4 @@ Windows
### 第二步
打开aliyunpan命令行程序,任何云盘命令都有类似如下日志输出
![](../assets/images/debug-log-screenshot.png)
## 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服务即可。
![](../assets/images/debug-log-screenshot.png)
+20 -6
View File
@@ -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.3
github.com/tickstep/bolt v1.3.3
github.com/tickstep/library-go v0.0.8
github.com/tickstep/aliyunpan-api v0.1.8
github.com/tickstep/bolt v1.3.4
github.com/tickstep/library-go v0.1.1
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
+46 -10
View File
@@ -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,34 @@ 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.3 h1:UlHJYiWyO8q8+shZaXGC0jVlnhLOk8dSTig/4vw9eJM=
github.com/tickstep/aliyunpan-api v0.1.3/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.8 h1:heK04T2F1K5eDSHDqHJ2yHA6RSBqyN/4yqYjHClbJvI=
github.com/tickstep/aliyunpan-api v0.1.8/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/go.mod h1:uAHeNOIpoywCzlaeLrWmmRSupn03m9kJVZKOEmuarmA=
github.com/tickstep/library-go v0.1.1 h1:xPAyddCJkZy5S3qUxCg0hoEG9PR8/rdIQiLMrt2OtWI=
github.com/tickstep/library-go v0.1.1/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 +135,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=
+250 -2
View File
@@ -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,7 +52,7 @@ func CmdAlbum() cli.Command {
return cli.Command{
Name: "album",
Aliases: []string{"abm"},
Usage: "相簿(Beta)",
Usage: "个人相册(Beta)",
UsageText: cmder.App().Name + " album",
Category: "阿里云盘",
Before: ReloadConfigFunc,
@@ -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()
}
}
+6 -1
View File
@@ -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,
@@ -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
+15 -1
View File
@@ -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,
@@ -226,9 +226,15 @@ func CmdConfig() cli.Command {
if c.IsSet("local_addrs") {
config.Config.SetLocalAddrs(c.String("local_addrs"))
}
if c.IsSet("ip_type") {
config.Config.SetPreferIPType(c.String("ip_type"))
}
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 {
@@ -279,10 +285,18 @@ func CmdConfig() cli.Command {
Name: "local_addrs",
Usage: "设置本地网卡地址, 多个地址用逗号隔开",
},
cli.StringFlag{
Name: "ip_type",
Usage: "设置域名解析IP优先类型",
},
cli.StringFlag{
Name: "file_record_config",
Usage: "设置是否开启上传、下载、同步文件的结果记录功能",
},
cli.StringFlag{
Name: "device_id",
Usage: "设置客户端ID,24位的字符串",
},
},
},
},
+4 -7
View File
@@ -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,
@@ -75,12 +75,9 @@ func CmdDownload() cli.Command {
UsageText: cmder.App().Name + " download <文件/目录路径1> <文件/目录2> <文件/目录3> ...",
Description: `
下载的文件默认保存到, 程序所在目录的 download/ 目录。支持软链接文件,包括Linux/macOS(ln命令)和Windows(mklink命令)创建的符号链接文件。
通过 aliyunpan config set -savedir <savedir>, 自定义保存的目录。
支持多个文件或目录下载.
自动跳过下载重名的文件!
通过 aliyunpan config set -savedir <savedir>, 自定义保存的目录。支持多个文件或目录下载,支持自动跳过下载重名的文件!
示例:
设置保存目录, 保存到 D:\Downloads
注意区别反斜杠 "\" 和 斜杠 "/" !!!
aliyunpan config set -savedir D:\\Downloads
@@ -234,7 +231,7 @@ func RunDownload(paths []string, options *DownloadOptions) {
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")
}
}
@@ -302,7 +299,7 @@ func RunDownload(paths []string, options *DownloadOptions) {
fmt.Println(err)
return
}
fmt.Printf("\n[*] 注意:由于阿里云盘接口的限制,当前不支持>100M单个文件的下载。")
fmt.Printf("\n[0] 当前文件下载最大并发量为: %d, 下载缓存为: %s\n\n", options.Parallel, converter.ConvertFileSize(int64(cfg.CacheSize), 2))
var (
+8 -3
View File
@@ -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,
@@ -27,9 +27,9 @@ import (
func CmdDrive() cli.Command {
return cli.Command{
Name: "drive",
Usage: "切换网盘(文件网盘/相册网盘)",
Usage: "切换网盘(备份盘/资源库/相册)",
Description: `
切换已登录的阿里云盘的云工作网盘(文件网盘/相册网盘)
切换已登录账号的阿里云盘的工作网盘(备份盘/资源库/相册)
如果运行该条命令没有提供参数, 程序将会列出所有的网盘列表, 供选择切换.
示例:
@@ -100,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 = "/"
+9 -3
View File
@@ -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,
@@ -60,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
@@ -116,6 +116,12 @@ func CmdLogout() cli.Command {
}
}
// 云端注销登录
//if _, e := activeUser.PanClient().DeviceLogout(); e != nil {
// fmt.Printf("登出设备失败,请手动在网页端进行登出: %s\n", e.String())
//}
// 删除用户信息
deletedUser, err := config.Config.DeleteUser(activeUser.UserId)
if err != nil {
fmt.Printf("退出用户 %s, 失败, 错误: %s\n", activeUser.Nickname, err)
@@ -141,7 +147,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()
+4 -7
View File
@@ -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 {
@@ -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)
}
}
}
+2 -2
View File
@@ -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,
@@ -28,7 +28,7 @@ func CmdMv() cli.Command {
return cli.Command{
Name: "mv",
Usage: "移动文件/目录",
UsageText: `移动:
UsageText: `
aliyunpan mv <文件/目录1> <文件/目录2> <文件/目录3> ... <目标目录>`,
Description: `
注意: 移动多个文件和目录时, 请确保每一个文件和目录都存在, 否则移动操作会失败。支持通配符匹配移动文件,通配符当前只能匹配文件名,不能匹配文件路径。
+2 -2
View File
@@ -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,
@@ -41,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))
+26 -34
View File
@@ -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,
@@ -24,6 +24,7 @@ import (
"github.com/urfave/cli"
"os"
"strconv"
"time"
)
func CmdRecycle() cli.Command {
@@ -218,39 +219,30 @@ func RunRecycleDelete(driveId string, fidStrList ...string) {
func RunRecycleClear(driveId string) {
panClient := GetActivePanClient()
for {
// get file list
fdl, err := panClient.RecycleBinFileListGetAll(&aliyunpan.RecycleBinFileListParam{
DriveId: driveId,
Limit: 100,
})
if err != nil {
logger.Verboseln(err)
break
}
if fdl == nil || len(fdl) == 0 {
break
}
// delete
deleteFileList := []*aliyunpan.FileBatchActionParam{}
for _, f := range fdl {
deleteFileList = append(deleteFileList, &aliyunpan.FileBatchActionParam{
DriveId: driveId,
FileId: f.FileId,
})
}
if len(deleteFileList) == 0 {
logger.Verboseln("没有需要删除的文件")
break
}
rbfr, err := panClient.RecycleBinFileDelete(deleteFileList)
if rbfr != nil && len(rbfr) > 0 {
logger.Verboseln("彻底删除文件成功")
}
// 提交清空回收站异步任务
r, err := panClient.RecycleBinFileClear(&aliyunpan.RecycleBinFileClearParam{
DriveId: driveId,
})
if err != nil {
logger.Verboseln(err)
fmt.Printf("当前无法清空回收站,请稍后重试\n")
}
fmt.Printf("清空回收站成功\n")
for i := 0; i < 10; i++ {
ar, err1 := panClient.AsyncTaskQueryStatus(&aliyunpan.AsyncTaskQueryStatusParam{
AsyncTaskId: r.AsyncTaskId,
})
if err1 != nil {
logger.Verboseln(err1)
time.Sleep(2 * time.Second)
continue
}
if ar.Status == "Succeed" {
fmt.Printf("清空回收站成功\n")
return
} else {
time.Sleep(1 * time.Second)
}
}
fmt.Printf("清空回收站失败,请稍后重试\n")
}
+23 -14
View File
@@ -77,6 +77,9 @@ func CmdRename() cli.Command {
4. 批量重命名,将当前目录下所有.mp4文件全部进行 "视频+编号.mp4" 的重命名操作,旧的名称全部去掉,即:视频001.mp4, 视频002.mp4, 视频003.mp4...
aliyunpan rename * 视频###.mp4 *.mp4
5. 批量重命名,将当前目录下所有.mp4文件全部进行 "视频+编号.mp4" 的重命名操作,旧的名称全部去掉,直接重命名无需人工确认操作
aliyunpan rename -y * 视频###.mp4 *.mp4
`,
Category: "阿里云盘",
Before: ReloadConfigFunc,
@@ -93,7 +96,7 @@ func CmdRename() cli.Command {
RunRename(parseDriveId(c), c.Args().Get(0), c.Args().Get(1))
} else if c.NArg() == 3 {
// 批量重命名
RunRenameBatch(parseDriveId(c), c.Args().Get(0), c.Args().Get(1), c.Args().Get(2))
RunRenameBatch(c.Bool("y"), parseDriveId(c), c.Args().Get(0), c.Args().Get(1), c.Args().Get(2))
}
return nil
},
@@ -103,6 +106,10 @@ func CmdRename() cli.Command {
Usage: "网盘ID",
Value: "",
},
cli.BoolFlag{
Name: "y",
Usage: "跳过人工确认,对批量操作有效",
},
},
}
}
@@ -150,7 +157,7 @@ func RunRename(driveId string, oldName string, newName string) {
}
// RunRenameBatch 批量重命名文件
func RunRenameBatch(driveId string, expression, replacement, filePattern string) {
func RunRenameBatch(skipConfirm bool, driveId string, expression, replacement, filePattern string) {
if len(expression) == 0 {
fmt.Println("旧文件名不能为空")
return
@@ -209,18 +216,20 @@ func RunRenameBatch(driveId string, expression, replacement, filePattern string)
}
// 确认
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
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
}
}
// 重命名
+67 -33
View File
@@ -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,
@@ -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],
+1 -1
View File
@@ -313,7 +313,7 @@ func RunSync(defaultTask *syncdrive.SyncTask, fileDownloadParallel, fileUploadPa
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)
}
+76 -11
View File
@@ -32,6 +32,12 @@ func CmdTree() cli.Command {
列出 /我的资源 内的文件和目录的树形图,并且显示文件对应的完整绝对路径
aliyunpan tree -fp /我的资源
列出 /我的资源 内的文件和目录的树形图,并且显示文件对应的文件大小
aliyunpan tree -fs /我的资源
列出 /我的资源 内的文件和目录的树形图,过滤大于等于 10kb 并且小于等于 10mb 的文件,同时显示文件对应的文件大小
aliyunpan tree -fs -minSize=1kb -maxSize=10mb /我的资源
`,
Category: "阿里云盘",
Before: ReloadConfigFunc,
@@ -40,7 +46,19 @@ func CmdTree() cli.Command {
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))
}
+25 -13
View File
@@ -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,
@@ -62,6 +62,7 @@ type (
NoRapidUpload bool
ShowProgress bool
IsOverwrite bool // 覆盖已存在的文件,如果同名文件已存在则移到回收站里
IsSkipSameName bool // 跳过已存在的文件,即使文件内容不一致(不检查SHA1)
DriveId string
ExcludeNames []string // 排除的文件名,包括文件夹和文件。即这些文件/文件夹不进行上传,支持正则表达式
BlockSize int64 // 分片大小
@@ -92,6 +93,10 @@ var UploadFlags = []cli.Flag{
Name: "ow",
Usage: "overwrite, 覆盖已存在的同名文件,注意已存在的文件会被移到回收站",
},
cli.BoolFlag{
Name: "skip",
Usage: "skip same name, 跳过已存在的同名文件,即使文件内容不一致(不检查SHA1)",
},
cli.BoolFlag{
Name: "norapid",
Usage: "不检测秒传。跳过费时的SHA1计算直接上传",
@@ -151,6 +156,9 @@ func CmdUpload() cli.Command {
9. 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的 @eadir 文件夹
aliyunpan upload -exn "^@eadir$" C:/Users/Administrator/Video /视频
10. 跳过已存在的同名文件,即使文件内容不一致(不检查SHA1)
aliyunpan upload -skip 1.mp4 /视频
参考:
以下是典型的排除特定文件或者文件夹的例子,注意:参数值必须是正则表达式。在正则表达式中,^表示匹配开头,$表示匹配结尾。
1)排除@eadir文件或者文件夹:-exn "^@eadir$"
@@ -186,16 +194,17 @@ func CmdUpload() cli.Command {
//}
RunUpload(subArgs[:c.NArg()-1], subArgs[c.NArg()-1], &UploadOptions{
AllParallel: c.Int("p"), // 多文件上传的时候,允许同时并行上传的文件数量
Parallel: 1, // 一个文件同时多少个线程并发上传的数量。阿里云盘只支持单线程按顺序进行文件part数据上传,所以只能是1
MaxRetry: c.Int("retry"),
MaxTimeoutSec: timeout,
NoRapidUpload: c.Bool("norapid"),
ShowProgress: !c.Bool("np"),
IsOverwrite: c.Bool("ow"),
DriveId: parseDriveId(c),
ExcludeNames: c.StringSlice("exn"),
BlockSize: int64(c.Int("bs") * 1024),
AllParallel: c.Int("p"), // 多文件上传的时候,允许同时并行上传的文件数量
Parallel: 1, // 一个文件同时多少个线程并发上传的数量。阿里云盘只支持单线程按顺序进行文件part数据上传,所以只能是1
MaxRetry: c.Int("retry"),
MaxTimeoutSec: timeout,
NoRapidUpload: c.Bool("norapid"),
ShowProgress: !c.Bool("np"),
IsOverwrite: c.Bool("ow"),
IsSkipSameName: c.Bool("skip"),
DriveId: parseDriveId(c),
ExcludeNames: c.StringSlice("exn"),
BlockSize: int64(c.Int("bs") * 1024),
})
// 释放文件锁
@@ -276,7 +285,7 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
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")
}
}
@@ -294,7 +303,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 {
@@ -441,6 +450,7 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
UploadStatistic: statistic,
ShowProgress: opt.ShowProgress,
IsOverwrite: opt.IsOverwrite,
IsSkipSameName: opt.IsSkipSameName,
UseInternalUrl: opt.UseInternalUrl,
GlobalSpeedsStat: globalSpeedsStat,
FileRecorder: fileRecorder,
@@ -451,6 +461,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)
@@ -461,6 +472,7 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
fmt.Printf("创建云盘文件夹失败: %s\n", saveFilePath)
}
}
folderCreateMutex.Unlock()
}
}
return nil
+7 -1
View File
@@ -129,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
}
@@ -167,6 +167,12 @@ func RefreshTokenInNeed(activeUser *config.PanUser) bool {
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
+1 -1
View File
@@ -85,7 +85,7 @@ aliyunpan webdav start -h
// 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")
}
}
+178
View File
@@ -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
}
+35 -4
View File
@@ -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 (
@@ -96,13 +99,17 @@ type PanConfig struct {
SaveDir string `json:"saveDir"` // 下载储存路径
Proxy string `json:"proxy"` // 代理
LocalAddrs string `json:"localAddrs"` // 本地网卡地址
Proxy string `json:"proxy"` // 代理
LocalAddrs string `json:"localAddrs"` // 本地网卡地址
PreferIPType string `json:"preferIPType"` // 优先IP类型,IPv4或者IPv6
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
fileMu sync.Mutex
@@ -195,6 +202,15 @@ func (c *PanConfig) init() error {
requester.SetLocalTCPAddrList(strings.Split(c.LocalAddrs, ",")...)
}
// 设置域名解析策略 IPv4 or IPv6
t := requester.IPAny
if strings.ToLower(c.PreferIPType) == "ipv4" {
t = requester.IPv4
} else if strings.ToLower(c.PreferIPType) == "ipv6" {
t = requester.IPv6
}
requester.SetPreferIPType(t)
return nil
}
@@ -251,6 +267,9 @@ func (c *PanConfig) loadConfigFromFile() (err error) {
if err != nil {
return ErrConfigContentsParseError
}
if c.DeviceName == "" {
c.DeviceName = DefaultDeviceName
}
return nil
}
@@ -273,7 +292,10 @@ func (c *PanConfig) initDefaultConfig() {
}
c.ConfigVer = ConfigVersion
c.VideoFileExtensions = DefaultVideoFileExtensions
c.DeviceId = RandomDeviceId() // 生成默认客户端ID
c.DeviceName = DefaultDeviceName
c.FileRecordConfig = "1" // 默认开启
c.PreferIPType = "ipv4" // 默认优先IPv4
}
// GetConfigDir 获取配置路径
@@ -368,7 +390,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
@@ -387,6 +409,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 {
+23 -1
View File
@@ -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,6 +36,17 @@ func (c *PanConfig) SetLocalAddrs(localAddrs string) {
requester.SetLocalTCPAddrList(strings.Split(localAddrs, ",")...)
}
func (c *PanConfig) SetPreferIPType(ipType string) {
c.PreferIPType = ipType
t := requester.IPAny
if strings.ToLower(ipType) == "ipv4" {
t = requester.IPv4
} else if strings.ToLower(ipType) == "ipv6" {
t = requester.IPv6
}
requester.SetPreferIPType(t)
}
// SetCacheSizeByStr 设置cache_size
func (c *PanConfig) SetCacheSizeByStr(sizeStr string) error {
size, err := converter.ParseFileSizeStr(sizeStr)
@@ -74,6 +85,15 @@ func (c *PanConfig) SetFileRecorderConfig(config string) error {
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 := "禁用"
@@ -94,7 +114,9 @@ func (c *PanConfig) PrintTable() {
[]string{"savedir", c.SaveDir, "", "下载文件的储存目录"},
[]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{"ip_type", c.PreferIPType, "ipv4-优先IPv4,ipv6-优先IPv6", "设置域名解析IP优先类型。修改后需要重启应用生效"},
[]string{"file_record_config", fileRecorderLabel, "1-开启,2-禁用", "设置是否开启上传、下载、同步文件的结果记录,开启后会把结果记录到CSV文件方便后期查看"},
[]string{"device_id", c.DeviceId, "", "客户端ID,用于标识登录客户端,阿里单个账号最多允许10个客户端同时在线。修改后需要重启应用生效"},
})
tb.Render()
}
+53 -4
View File
@@ -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"
}
+14
View File
@@ -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()
}
+4
View File
@@ -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())
}
+12
View File
@@ -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)
@@ -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,6 +43,8 @@ import (
)
type (
FileSourceType string
// DownloadTaskUnit 下载的任务单元
DownloadTaskUnit struct {
taskInfo *taskframework.TaskInfo // 任务信息
@@ -67,7 +69,8 @@ type (
OriginSaveRootPath string // 文件保存在本地的根目录路径
DriveId string
fileInfo *aliyunpan.FileEntity // 文件或目录详情
fileInfo *aliyunpan.FileEntity // 文件或目录详情
downloadFileSource FileSourceType // 下载项类型,File-普通文件(备份盘/资源库),Album-相册文件
// 下载文件记录器
FileRecorder *log.FileRecorder
@@ -89,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
}
@@ -209,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,
@@ -275,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 {
@@ -315,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 {
// 不检测文件有效性
@@ -376,12 +393,14 @@ func (dtu *DownloadTaskUnit) OnSuccess(lastRunResult *taskframework.TaskUnitRunR
// 下载文件数据记录
if config.Config.FileRecordConfig == "1" {
if dtu.fileInfo.IsFile() {
dtu.FileRecorder.Append(&log.FileRecordItem{
Status: "成功",
TimeStr: utils.NowTimeStr(),
FileSize: dtu.fileInfo.FileSize,
FilePath: dtu.fileInfo.Path,
})
if dtu.FileRecorder != nil {
dtu.FileRecorder.Append(&log.FileRecordItem{
Status: "成功",
TimeStr: utils.NowTimeStr(),
FileSize: dtu.fileInfo.FileSize,
FilePath: dtu.fileInfo.Path,
})
}
}
}
}
@@ -438,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)
}
// 输出文件信息
@@ -618,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)
// 下载成功
+13
View File
@@ -238,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
}
@@ -64,8 +64,9 @@ type (
panFile string
state *uploader.InstanceState
ShowProgress bool
IsOverwrite bool // 覆盖已存在的文件,如果同名文件已存在则移到回收站里
ShowProgress bool
IsOverwrite bool // 覆盖已存在的文件,如果同名文件已存在则移到回收站里
IsSkipSameName bool // 跳过已存在的文件,即使文件内容不一致(不检查SHA1)
// 是否使用内置链接
UseInternalUrl bool
@@ -179,9 +180,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(),
@@ -325,6 +328,7 @@ func (utu *UploadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
var apierr *apierror.ApiError
var rs *aliyunpan.MkdirResult
var efi *aliyunpan.FileEntity
var appCreateUploadFileParam *aliyunpan.CreateFileUploadParam
var sha1Str string
var contentHashName string
@@ -350,24 +354,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 = ""
@@ -378,6 +400,23 @@ StepUploadPrepareUpload:
proofCode = ""
contentHashName = "sha1"
checkNameMode = "auto_rename"
// 如果启用了 覆盖/跳过 已存在的文件,则需要提前检查文件是否存在
if utu.IsOverwrite || utu.IsSkipSameName {
efi, apierr = utu.PanClient.FileInfoByPath(utu.DriveId, utu.SavePath)
if apierr != nil && apierr.Code != apierror.ApiCodeFileNotFoundCode {
result.Err = apierr
result.ResultMessage = "检测同名文件失败"
return
}
}
if utu.IsSkipSameName {
if efi != nil && efi.FileId != "" {
result.Succeed = true
result.Extra = efi
fmt.Printf("[%s] %s 检测到同名文件,跳过上传: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.SavePath)
return
}
}
if !utu.NoRapidUpload {
// 计算文件SHA1
fmt.Printf("[%s] %s 正在计算文件SHA1: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.LocalFileChecksum.Path.LogicPath)
@@ -401,12 +440,6 @@ StepUploadPrepareUpload:
if utu.IsOverwrite {
// 标记覆盖旧同名文件
// 检查同名文件是否存在
efi, apierr := utu.PanClient.FileInfoByPath(utu.DriveId, utu.SavePath)
if apierr != nil && apierr.Code != apierror.ApiCodeFileNotFoundCode {
result.Err = apierr
result.ResultMessage = "检测同名文件失败"
return
}
if efi != nil && efi.FileId != "" {
if strings.ToUpper(efi.ContentHash) == strings.ToUpper(sha1Str) {
result.Succeed = true
@@ -424,7 +457,7 @@ StepUploadPrepareUpload:
return
}
time.Sleep(time.Duration(500) * time.Millisecond)
fmt.Printf("[%s] %s 检测到同名文件,已移动到回收站: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.SavePath)
fmt.Printf("[%s] %s 检测到同名文件,文件内容不一致,已将旧文件移动到回收站: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.SavePath)
}
}
@@ -450,6 +483,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()
+8 -6
View File
@@ -95,12 +95,14 @@ func (f *FileActionTask) DoAction(ctx context.Context) error {
}
// recorder
f.appendRecord(&log.FileRecordItem{
Status: "成功-上传",
TimeStr: utils.NowTimeStr(),
FileSize: actFile.FileSize,
FilePath: actFile.Path,
})
if actFile != nil {
f.appendRecord(&log.FileRecordItem{
Status: "成功-上传",
TimeStr: utils.NowTimeStr(),
FileSize: actFile.FileSize,
FilePath: actFile.Path,
})
}
}
}
@@ -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",
+10 -1
View File
@@ -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()
+12 -3
View File
@@ -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)
}
+10 -1
View File
@@ -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",
+41 -10
View File
@@ -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.5"
Version = "v0.2.9"
// 命令历史文件
historyFilePath = filepath.Join(config.GetConfigDir(), "aliyunpan_command_history.txt")
@@ -68,7 +69,7 @@ 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)
}
@@ -82,7 +83,7 @@ func checkLoginExpiredAndRelogin() {
command.TryLogin()
} else {
// 刷新过期Token并保存到配置文件
command.RefreshTokenInNeed(activeUser)
command.RefreshTokenInNeed(activeUser, config.Config.DeviceName)
}
command.SaveConfigFunc(nil)
}
@@ -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-2023 tickstep."
app.Copyright = "(c) 2021-2024 tickstep."
app.Usage = "阿里云盘客户端 for " + runtime.GOOS + "/" + runtime.GOARCH
app.Description = `aliyunpan 是一款阿里云盘命令行客户端工具, 为操作阿里云盘, 提供实用功能。
支持webdav文件协议,可以让阿里云盘变身为webdav协议的文件服务器,用于挂载作为Windows、Linux、Mac系统的磁盘进行使用。
支持同步备份功能,支持备份本地文件到云盘,备份云盘文件到本地,双向同步备份。
具体功能, 参见 COMMANDS 列表。
@@ -252,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
}
@@ -346,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 + "(相册)$ "
@@ -433,6 +438,9 @@ func main() {
//// 拷贝文件/目录 cp
//command.CmdCp(),
// 备份盘和资源库之间拷贝文件 xcp
command.CmdXcp(),
// 移动文件/目录 mv
command.CmdMv(),
@@ -455,7 +463,7 @@ func main() {
command.CmdDownload(),
// 获取文件下载链接
command.CmdLocateUrl(),
//command.CmdLocateUrl(),
// 导出文件/目录元数据 export
//command.CmdExport(),
@@ -463,8 +471,8 @@ func main() {
// 导入文件 import
//command.CmdImport(),
// webdav服务
command.CmdWebdav(),
// webdav服务(depressed)
//command.CmdWebdav(),
// 回收站
command.CmdRecycle(),
@@ -600,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
View File
@@ -3,13 +3,13 @@
"FileVersion": {
"Major": 0,
"Minor": 2,
"Patch": 5,
"Patch": 9,
"Build": 0
},
"ProductVersion": {
"Major": 0,
"Minor": 2,
"Patch": 5,
"Patch": 9,
"Build": 0
},
"FileFlagsMask": "3f",
@@ -22,14 +22,14 @@
"Comments": "",
"CompanyName": "tickstep",
"FileDescription": "阿里云盘客户端",
"FileVersion": "v0.2.5",
"FileVersion": "v0.2.9",
"InternalName": "",
"LegalCopyright": "© 2021-2023 tickstep.",
"LegalCopyright": "© 2021-2024 tickstep.",
"LegalTrademarks": "",
"OriginalFilename": "",
"PrivateBuild": "",
"ProductName": "aliyunpan",
"ProductVersion": "v0.2.5",
"ProductVersion": "v0.2.9",
"SpecialBuild": ""
},
"VarFileInfo": {