Compare commits

...

109 Commits

Author SHA1 Message Date
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
tickstep c9c33217d2 update version to v0.2.5 2023-01-01 14:08:28 +08:00
tickstep 79dcf40792 support batch rename #215 2023-01-01 13:42:49 +08:00
tickstep d618686da9 add tips for proxy config 2022-12-29 11:14:31 +08:00
tickstep 5f52145378 update aliyunpan-api to v0.1.3 2022-12-29 10:46:02 +08:00
tickstep bd958fb8df remove file locker 2022-12-27 18:58:49 +08:00
tickstep 8fc8d9b6f7 fix tab error #212 2022-12-27 17:19:22 +08:00
tickstep 70ddb7e0c1 expland expired time to fix url expired issue #214 #207 #208 2022-12-27 10:06:19 +08:00
tickstep 0acd6a43d6 fix URL expired issue #208 2022-12-26 21:24:07 +08:00
tickstep 97a22812a2 fix LS commnad list root folder error 2022-12-24 17:10:07 +08:00
tickstep 118e521d63 fix UploadPartNotSeq issue #208 2022-12-24 17:04:38 +08:00
tickstep f8c4961608 remove upload useless code 2022-12-21 22:08:32 +08:00
xiaoyaofenfen ed38002e65 fix token refresh routine not exit error 2022-12-21 16:27:39 +08:00
xiaoyaofenfen b74d39da41 change config tips 2022-12-21 15:44:20 +08:00
xiaoyaofenfen aabab869c4 show current config folder for config command 2022-12-21 15:41:00 +08:00
xiaoyaofenfen bb8dad7262 make download/upload/sync command singleton #208 #207 2022-12-21 15:30:35 +08:00
xiaoyaofenfen 9f9fb6a320 change log folder permission 2022-12-21 15:29:15 +08:00
xiaoyaofenfen 84d7ec923f remove rapid upload limit 2022-12-20 11:54:20 +08:00
xiaoyaofenfen 07746fc15c add sync file record #206 2022-12-19 20:41:05 +08:00
xiaoyaofenfen c5d15b058a add download file record #206 2022-12-19 19:50:33 +08:00
xiaoyaofenfen ea9279e45e add upload file record #206 2022-12-19 19:41:13 +08:00
xiaoyaofenfen 6abab46db0 add file recorder config 2022-12-19 19:40:42 +08:00
xiaoyaofenfen 6e8ee94285 add file recorder library 2022-12-19 19:17:31 +08:00
xiaoyaofenfen 1208ccad08 add file recorder library 2022-12-19 19:16:39 +08:00
xiaoyaofenfen d23802e8cc auto resize upload block size for big file #205 2022-12-13 16:32:08 +08:00
xiaoyaofenfen 59646b07fe add upload block size usage documents 2022-12-13 09:54:22 +08:00
xiaoyaofenfen 34dd8a98fe donot load js file everytime 2022-12-13 09:53:29 +08:00
xiaoyaofenfen b6780931cc fix webdav token plugin callback error #203 2022-12-12 14:07:58 +08:00
xiaoyaofenfen fb705365b1 add token plugin callback for login routine #203 2022-12-10 14:47:00 +08:00
xiaoyaofenfen 1c6f6c772c refractor login routine 2022-12-10 14:07:28 +08:00
xiaoyaofenfen 7726888365 add email notify sample for manual document 2022-12-08 15:59:16 +08:00
tickstep eda44658c9 update version to v0.2.4 2022-12-07 22:01:23 +08:00
tickstep 0b6c77b96f update aliyunpan-api version 2022-12-07 21:45:39 +08:00
tickstep f88e921b04 add kv dictionary for plugin 2022-12-07 21:31:33 +08:00
xiaoyaofenfen c3dadd00b7 add token plugin callback for token refresh routine 2022-12-07 17:35:03 +08:00
xiaoyaofenfen 90a889f0eb fix webdav token refresh erorr 2022-12-07 17:24:02 +08:00
xiaoyaofenfen 5935f78824 fix token refresh update bug #167 2022-12-07 17:09:24 +08:00
xiaoyaofenfen 56ba938c72 add userTokenRefreshFinishCallback plugin api #172 2022-12-07 16:45:59 +08:00
xiaoyaofenfen 6ade77c4bf add send email api for plugin 2022-12-07 10:34:46 +08:00
xiaoyaofenfen ecacda2596 update manual doc 2022-12-07 09:32:13 +08:00
xiaoyaofenfen 695acce3f4 add send email api for plugin 2022-12-06 20:40:19 +08:00
xiaoyaofenfen 98de5b931c fix too many requests error for upload 2022-12-06 19:48:31 +08:00
xiaoyaofenfen a1577f19c5 update readme file 2022-12-06 15:40:30 +08:00
xiaoyaofenfen f1fb75cfe4 short token refresh check time 2022-12-06 13:46:19 +08:00
xiaoyaofenfen 7ee48fa726 download command support exclude name patternt 2022-12-06 11:28:36 +08:00
xiaoyaofenfen 15084b3ac2 refactor upload exclude file method 2022-12-06 09:52:12 +08:00
xiaoyaofenfen 4a4796218e download command support wildcard pattern 2022-12-06 09:29:49 +08:00
xiaoyaofenfen 9ccaf8a25d add usage 2022-12-06 08:41:07 +08:00
xiaoyaofenfen dc469e87d2 share set command support wildcard pattern 2022-12-05 20:43:35 +08:00
xiaoyaofenfen 67c288318e locate command support wildcard pattern 2022-12-05 20:25:35 +08:00
xiaoyaofenfen 36a5eed5af tree command support wildcard pattern 2022-12-05 20:19:52 +08:00
xiaoyaofenfen fb24f703c2 rm command support wildcard pattern 2022-12-05 20:10:02 +08:00
xiaoyaofenfen 0d203ece9a mv command support wildcard pattern 2022-12-05 20:04:36 +08:00
xiaoyaofenfen 9be7080ae3 fix bad gateway error 2022-12-05 19:59:37 +08:00
xiaoyaofenfen b8aeead58c ls command support wildcard patterns 2022-12-05 17:25:21 +08:00
xiaoyaofenfen a15f704672 cd command support wildcard patterns 2022-12-05 17:14:03 +08:00
xiaoyaofenfen 4173889120 add wildchar path match method 2022-12-05 16:56:17 +08:00
xiaoyaofenfen 077d18f471 fix QrCode login error 2022-12-01 14:13:08 +08:00
xiaoyaofenfen 1c454060c1 handle sync delete file error #199 2022-12-01 11:39:37 +08:00
xiaoyaofenfen a455af2084 handle download delete file error #199 2022-12-01 11:32:32 +08:00
xiaoyaofenfen 5f6cdcab64 fix upload file no permission error for sync #188 2022-11-14 10:26:31 +08:00
xiaoyaofenfen 9db89a64b0 Merge pull request #190 from ZemingLiu/main
set supported architectures in apt source file
2022-11-12 10:35:23 +08:00
Zeming.Liu b600dd17c7 set supported architectures in apt source file 2022-11-12 01:34:13 +08:00
tickstep 3b18f9af78 fix complie error for go18+ sdk #175 2022-11-05 18:13:10 +08:00
tickstep 00e1c3e3d4 fix illegal download file issue #183 2022-11-02 21:52:11 +08:00
xiaoyaofenfen 2e4a6d94df modify mips build script #182 2022-10-31 17:13:32 +08:00
xiaoyaofenfen bf91a47009 add userId for sync task #176 2022-10-28 10:40:59 +08:00
xiaoyaofenfen a2f33f32bf fix share error message missing bug 2022-10-18 09:14:40 +08:00
xiaoyaofenfen f511e17091 fix download file error for upgrade 2022-10-17 20:01:36 +08:00
tickstep 3ad528bc8e update version to v0.2.3 2022-10-16 09:23:45 +08:00
tickstep 1c80403bc3 support get download url for folder 2022-10-16 09:15:37 +08:00
tickstep 8c08a322a1 update aliyunpan-api module 2022-10-15 22:37:03 +08:00
tickstep a2a46bdeeb locate can save download links to file 2022-10-15 22:25:22 +08:00
tickstep ae077101f1 fix update check error 2022-10-15 16:49:17 +08:00
xiaoyaofenfen 41fd68411e add document for locate command 2022-10-14 15:21:50 +08:00
xiaoyaofenfen a81d97c8bc add timeout option for upload #157 2022-10-14 14:22:28 +08:00
xiaoyaofenfen 13451ccf4c add locate command to get directly download url for file 2022-10-14 11:03:40 +08:00
xiaoyaofenfen 643d35a122 refactor mv to support wildcard pattern 2022-10-14 09:32:11 +08:00
xiaoyaofenfen 79833da4fc refactor rm to support wildcard pettern 2022-10-14 09:16:35 +08:00
xiaoyaofenfen f90829aaab add sync file finish callback for plugin #166 2022-10-10 11:35:40 +08:00
tickstep 072e7e9261 add total file size statistic for tree 2022-10-09 19:09:17 +08:00
tickstep b27e9b24d7 remove useless code 2022-10-09 19:08:47 +08:00
tickstep 8f79a7e807 support wildcard for move command #89 2022-10-07 12:11:34 +08:00
tickstep 81d7bd22c9 support wildcard for remove command 2022-10-07 11:41:27 +08:00
tickstep 47e8b004b8 add memo for download 2022-10-01 16:23:18 +08:00
89 changed files with 2880 additions and 580 deletions
+2 -1
View File
@@ -43,4 +43,5 @@ account.txt
git_push.sh
git_pull.sh
docker/sync/data
clear_docker.sh
clear_docker.sh
upload.sh
+57 -26
View File
@@ -8,33 +8,40 @@
4. [下载](docs/manual.md#下载文件目录)网盘内文件, 支持多个文件或目录下载, 支持断点续传和单文件并行下载。支持软链接(符号链接)文件。
5. [上传](docs/manual.md#上传文件目录)本地文件, 支持多个文件或目录上传,支持排除指定文件夹/文件(正则表达式)功能。支持软链接(符号链接)文件。
6. [同步备份功能](docs/manual.md#同步备份功能)支持备份本地文件到云盘,备份云盘文件到本地,双向同步备份保持本地文件和网盘文件同步。常用于嵌入式或者NAS等设备,支持docker镜像部署。
7. 命令和文件路径输入支持Tab键自动补全
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插件),你可以按照自己的需要定制上传/下载中关键步骤的行为,最大程度满足自己的个性化需求
# 目录
- [关于](#关于)
- [特色](#特色)
- [目录](#目录)
- [如何安装](#如何安装)
* [直接下载安装](#直接下载安装)
* [apt安装](#apt安装)
* [yum安装](#yum安装)
* [docker安装](#docker安装)
+ [sync同步盘](#sync同步盘)
+ [webdav共享盘](#webdav共享盘)
- [直接下载安装](#直接下载安装)
- [apt安装](#apt安装)
- [yum安装](#yum安装)
- [winget安装](#winget安装)
- [docker安装](#docker安装)
- [sync同步盘](#sync同步盘)
- [webdav共享盘](#webdav共享盘)
- [如何使用](#如何使用)
* [基本使用](#基本使用)
+ [修改配置目录](#修改配置目录)
+ [启动程序](#启动程序)
+ [查看帮助](#查看帮助)
+ [登录](#登录)
+ [查看文件列表](#查看文件列表)
+ [下载文件](#下载文件)
+ [上传文件](#上传文件)
+ [同步备份文件](#同步备份文件)
* [更多命令](#更多命令)
- [基本使用](#基本使用)
- [修改配置目录](#修改配置目录)
- [启动程序](#启动程序)
- [查看帮助](#查看帮助)
- [登录](#登录)
- [查看文件列表](#查看文件列表)
- [下载文件](#下载文件)
- [上传文件](#上传文件)
- [同步备份文件](#同步备份文件)
- [更多命令](#更多命令)
- [常见问题](#常见问题)
* [如何获取RefreshToken](#如何获取RefreshToken)
* [如何开启Debug调试日志](#如何开启Debug调试日志)
- [如何获取RefreshToken](#如何获取refreshtoken)
- [如何开启Debug调试日志](#如何开启debug调试日志)
- [第一步](#第一步)
- [第二步](#第二步)
- [如何登出和下线客户端](#如何登出和下线客户端)
- [交流反馈](#交流反馈)
- [鸣谢](#鸣谢)
@@ -53,16 +60,16 @@
参考例子:
```shell
wget https://github.com/tickstep/aliyunpan/releases/download/v0.2.2/aliyunpan-v0.2.2-linux-amd64.zip
unzip aliyunpan-v0.2.2-linux-amd64.zip
cd aliyunpan-v0.2.2-linux-amd64
wget https://github.com/tickstep/aliyunpan/releases/download/v0.2.7/aliyunpan-v0.2.7-linux-amd64.zip
unzip aliyunpan-v0.2.7-linux-amd64.zip
cd aliyunpan-v0.2.7-linux-amd64
./aliyunpan
```
## apt安装
适用于apt包管理器的系统,例如Ubuntu,国产deepin深度操作系统等。目前只支持amd64和arm64架构的机器。
```shell
sudo curl -fsSL http://file.tickstep.com/apt/pgp | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/tickstep-packages-archive-keyring.gpg > /dev/null && echo "deb [signed-by=/etc/apt/trusted.gpg.d/tickstep-packages-archive-keyring.gpg] http://file.tickstep.com/apt aliyunpan main" | sudo tee /etc/apt/sources.list.d/tickstep-aliyunpan.list > /dev/null && sudo apt-get update && sudo apt-get install -y aliyunpan
sudo curl -fsSL http://file.tickstep.com/apt/pgp | gpg --dearmor | sudo tee /etc/apt/trusted.gpg.d/tickstep-packages-archive-keyring.gpg > /dev/null && echo "deb [signed-by=/etc/apt/trusted.gpg.d/tickstep-packages-archive-keyring.gpg arch=amd64,arm64] http://file.tickstep.com/apt aliyunpan main" | sudo tee /etc/apt/sources.list.d/tickstep-aliyunpan.list > /dev/null && sudo apt-get update && sudo apt-get install -y aliyunpan
```
@@ -73,6 +80,20 @@ sudo curl -fsSL http://file.tickstep.com/rpm/aliyunpan/aliyunpan.repo | sudo tee
```
## winget安装
适用于Windows系统的winget包管理器。目前只支持x86和x64架构的机器。
更新源
```powershell
winget source update
```
安装
```powershell
winget install tickstep.aliyunpan --silent
```
## docker安装
### sync同步盘
同步备份功能,支持备份本地文件到云盘,备份云盘文件到本地,双向同步备份三种模式。支持JavaScript插件对备份文件进行过滤。
@@ -81,7 +102,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.2
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.7
<your local dir>:本地目录绝对路径,例如:/tickstep/Documents/设计文档
@@ -95,7 +116,7 @@ ALIYUNPAN_TASK_STEP:任务步骤, 支持两种: scan(只扫描并建立同步
### 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.2
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.7
ALIYUNPAN_REFRESH_TOKEN RefreshToken
@@ -307,6 +328,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 +344,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.2.0" processorArchitecture="*" name="com.tickstep.aliyunpan" type="win32"/>
<assemblyIdentity version="0.2.7.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

+55
View File
@@ -151,4 +151,59 @@ function syncScanPanFilePrepareCallback(context, params) {
// }
return result;
}
// ------------------------------------------------------------------------------------------
// 函数说明:同步备份-同步文件后的回调函数
//
// 参数说明
// context - 当前调用的上下文信息
// {
// "appName": "aliyunpan",
// "version": "v0.1.3",
// "userId": "11001d48564f43b3bc5662874f04bb11",
// "nickname": "tickstep",
// "fileDriveId": "19519111",
// "albumDriveId": "29519122"
// }
// appName - 应用名称,当前固定为aliyunpan
// version - 版本号
// userId - 当前登录用户的ID
// nickname - 用户昵称
// fileDriveId - 用户文件网盘ID
// albumDriveId - 用户相册网盘ID
//
// params - 同步文件后的参数
// {
// "action": "upload",
// "actionResult": "success",
// "driveId": "19519221",
// "fileName": "1.txt",
// "filePath": "D:/goprojects/dev/upload/1.txt",
// "fileSha1": "294C8F24C56042710813E95C55A0B018299BA9A7",
// "fileSize": "28077",
// "fileType": "file",
// "fileUpdatedAt": "2022-03-04 15:19:47"
// }
// action - 同步动作,
// download-下载文件,
// upload-上传文件,
// delete_local-删除本地文件,
// delete_pan-删除云盘文件,
// create_local_folder-创建本地文件夹,
// create_pan_folder-创建云盘文件夹
// actionResult - 同步结果,success-成功,fail-失败
// driveId - 网盘ID
// fileName - 文件名称
// filePath - 文件完整路径
// fileSha1 - 文件SHA1
// fileSize - 文件大小,单位B
// fileType - 文件类型,file-文件,folder-文件夹
// fileUpdatedAt - 文件修改时间
//
// 返回值说明
// (无)
// ------------------------------------------------------------------------------------------
function syncFileFinishCallback(context, params) {
console.log(params)
}
+48
View File
@@ -0,0 +1,48 @@
// ==========================================================================================
// aliyunpan JS插件回调处理函数
// 支持 JavaScript ECMAScript 5.1 语言规范
//
// 更多内容请查看官方文档:https://github.com/tickstep/aliyunpan
// ==========================================================================================
// ------------------------------------------------------------------------------------------
// 函数说明:用户Token刷新完成后回调函数
//
// 参数说明
// context - 当前调用的上下文信息
// {
// "appName": "aliyunpan",
// "version": "v0.1.3",
// "userId": "11001d48564f43b3bc5662874f04bb11",
// "nickname": "tickstep",
// "fileDriveId": "19519111",
// "albumDriveId": "29519122"
// }
// appName - 应用名称,当前固定为aliyunpan
// version - 版本号
// userId - 当前登录用户的ID
// nickname - 用户昵称
// fileDriveId - 用户文件网盘ID
// albumDriveId - 用户相册网盘ID
//
// params - Token刷新参数
// {
// "result": "success",
// "message": "ok",
// "oldToken": "aa31fcc229c54d5ab6d8bfb17aff3711",
// "newToken": "bb31fcc229c54d5ab6d8bfb17aff3722",
// "updatedAt": "2022-04-14 07:05:12"
// }
// result - Token刷新的结果,success-成功,fail-失败
// message - 消息说明,如果失败这里会有原因说明
// oldToken - 刷新前的Token
// newToken - 刷新后的Token,只有result为success的才有该值
// updatedAt - Token刷新的时间
//
// 返回值说明
// (没有返回值)
// ------------------------------------------------------------------------------------------
function userTokenRefreshFinishCallback(context, params) {
console.log(params)
}
+25 -14
View File
@@ -94,45 +94,56 @@ RicePack() {
return # 已取消web功能
}
# Android
############### Android ###############
export ANDROID_NDK_ROOT=/Users/tickstep/Applications/android_ndk/android-ndk-r23-darwin
CC=$ANDROID_NDK_ROOT/bin/arm-linux-androideabi/bin/clang AndroidBuild $name-$version"-android-api16-armv7" android arm 7
CC=$ANDROID_NDK_ROOT/bin/aarch64-linux-android/bin/clang AndroidBuild $name-$version"-android-api21-arm64" android arm64 7
CC=$ANDROID_NDK_ROOT/bin/i686-linux-android/bin/clang AndroidBuild $name-$version"-android-api16-386" android 386 7
CC=$ANDROID_NDK_ROOT/bin/x86_64-linux-android/bin/clang AndroidBuild $name-$version"-android-api21-amd64" android amd64 7
# iOS
############### iOS ###############
IOSBuild $name-$version"-ios-arm64"
# OS X / macOS
############### macOS ###############
Build $name-$version"-darwin-macos-amd64" darwin amd64
# Build $name-$version"-darwin-macos-386" darwin 386
Build $name-$version"-darwin-macos-arm64" darwin arm64
# Windows
############### Windows ###############
Build $name-$version"-windows-x86" windows 386
Build $name-$version"-windows-x64" windows amd64
Build $name-$version"-windows-arm" windows arm
# Linux
############### Linux ###############
# x64/x86
Build $name-$version"-linux-386" linux 386
Build $name-$version"-linux-amd64" linux amd64
# arm
Build $name-$version"-linux-armv5" linux arm 5
Build $name-$version"-linux-armv7" linux arm 7
Build $name-$version"-linux-arm64" linux arm64
GOMIPS=softfloat Build $name-$version"-linux-mips" linux mips
Build $name-$version"-linux-mips64" linux mips64
GOMIPS=softfloat Build $name-$version"-linux-mipsle" linux mipsle
Build $name-$version"-linux-mips64le" linux mips64le
# Build $name-$version"-linux-ppc64" linux ppc64
# Build $name-$version"-linux-ppc64le" linux ppc64le
# Build $name-$version"-linux-s390x" linux s390x
# Others
# Build $name-$version"-solaris-amd64" solaris amd64
# mips
GOMIPS=softfloat Build $name-$version"-linux-mips64" linux mips64
GOMIPS=softfloat Build $name-$version"-linux-mips64le" linux mips64le
GOMIPS=hardfloat Build $name-$version"-linux-mips64hf" linux mips64
GOMIPS=hardfloat Build $name-$version"-linux-mips64lehf" linux mips64le
GOMIPS=softfloat Build $name-$version"-linux-mips" linux mips
GOMIPS=softfloat Build $name-$version"-linux-mipsle" linux mipsle
GOMIPS=hardfloat Build $name-$version"-linux-mipshf" linux mips
GOMIPS=hardfloat Build $name-$version"-linux-mipslehf" linux mipsle
# freebsd
Build $name-$version"-freebsd-386" freebsd 386
Build $name-$version"-freebsd-amd64" freebsd amd64
# Build $name-$version"-freebsd-arm" freebsd arm
# Others
# Build $name-$version"-linux-ppc64" linux ppc64
# Build $name-$version"-linux-ppc64le" linux ppc64le
# Build $name-$version"-linux-s390x" linux s390x
# Build $name-$version"-solaris-amd64" solaris amd64
# Build $name-$version"-netbsd-386" netbsd 386
# Build $name-$version"-netbsd-amd64" netbsd amd64
# Build $name-$version"-netbsd-arm" netbsd arm
-98
View File
@@ -1,39 +1,11 @@
package cmder
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
"github.com/tickstep/aliyunpan/cmder/cmdliner"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/internal/functions/panlogin"
"github.com/tickstep/library-go/logger"
"github.com/urfave/cli"
"sync"
)
var (
appInstance *cli.App
saveConfigMutex *sync.Mutex = new(sync.Mutex)
ReloadConfigFunc = func(c *cli.Context) error {
err := config.Config.Reload()
if err != nil {
fmt.Printf("重载配置错误: %s\n", err)
}
return nil
}
SaveConfigFunc = func(c *cli.Context) error {
saveConfigMutex.Lock()
defer saveConfigMutex.Unlock()
err := config.Config.Save()
if err != nil {
fmt.Printf("保存配置错误: %s\n", err)
}
return nil
}
)
func SetApp(app *cli.App) {
@@ -43,73 +15,3 @@ func SetApp(app *cli.App) {
func App() *cli.App {
return appInstance
}
func DoLoginHelper(refreshToken string) (refreshTokenStr string, webToken aliyunpan.WebLoginToken, error error) {
line := cmdliner.NewLiner()
defer line.Close()
if refreshToken == "" {
refreshToken, error = line.State.Prompt("请输入RefreshToken, 回车键提交 > ")
if error != nil {
return
}
}
// app login
atoken, apperr := aliyunpan.GetAccessTokenFromRefreshToken(refreshToken)
if apperr != nil {
if apperr.Code == apierror.ApiCodeTokenExpiredCode || apperr.Code == apierror.ApiCodeRefreshTokenExpiredCode {
fmt.Println("Token过期,需要重新登录")
} else {
fmt.Println("Token登录失败:", apperr)
}
return "", webToken, fmt.Errorf("登录失败")
}
refreshTokenStr = refreshToken
return refreshTokenStr, *atoken, nil
}
func TryLogin() *config.PanUser {
// can do automatically login?
for _, u := range config.Config.UserList {
if u.UserId == config.Config.ActiveUID {
// login
_, webToken, err := DoLoginHelper(u.RefreshToken)
if err != nil {
logger.Verboseln("automatically login use saved refresh token error ", err)
if u.TokenId != "" {
logger.Verboseln("try to login use tokenId")
h := panlogin.NewLoginHelper(config.DefaultTokenServiceWebHost)
r, e := h.GetRefreshToken(u.TokenId)
if e != nil {
logger.Verboseln("try to login use tokenId error", e)
break
}
refreshToken, e := h.ParseSecureRefreshToken("", r.SecureRefreshToken)
if e != nil {
logger.Verboseln("try to parse refresh token error", e)
break
}
_, webToken, err = DoLoginHelper(refreshToken)
if err != nil {
logger.Verboseln("try to use refresh token from tokenId error", e)
break
}
fmt.Println("Token重新自动登录成功")
// save new refresh token
u.RefreshToken = refreshToken
}
break
}
// success
u.WebToken = webToken
// save
SaveConfigFunc(nil)
// reload
ReloadConfigFunc(nil)
return config.Config.ActiveUser()
}
}
return nil
}
+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
+18 -1
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 ]
@@ -39,5 +48,13 @@ else
./aliyunpan login -RefreshToken=${ALIYUNPAN_REFRESH_TOKEN}
fi
./aliyunpan config set -transfer_url_type ${ALIYUNPAN_TRANSFER_URL_TYPE}
./aliyunpan config set -transfer_url_type ${ALIYUNPAN_TRANSFER_URL_TYPE}
if [ "$ALIYUNPAN_SYNC_LOG" = "true" ]
then
./aliyunpan config set -file_record_config 1
else
./aliyunpan config set -file_record_config 2
fi
./aliyunpan sync start -dp ${ALIYUNPAN_DOWNLOAD_PARALLEL} -up ${ALIYUNPAN_UPLOAD_PARALLEL} -dbs ${ALIYUNPAN_DOWNLOAD_BLOCK_SIZE} -ubs ${ALIYUNPAN_UPLOAD_BLOCK_SIZE} -log ${ALIYUNPAN_SYNC_LOG} -ldt ${ALIYUNPAN_LOCAL_DELAY_TIME} -step ${ALIYUNPAN_TASK_STEP}
+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=
+179 -8
View File
@@ -16,6 +16,7 @@
* [输出工作目录](#输出工作目录)
* [列出目录](#列出目录)
* [下载文件/目录](#下载文件目录)
* [获取文件下载直链](#获取文件下载直链)
* [上传文件/目录](#上传文件目录)
* [手动秒传文件](#手动秒传文件)
* [创建目录](#创建目录)
@@ -49,6 +50,8 @@
+ [1.禁止特定文件上传](#1.禁止特定文件上传)
+ [2.上传文件后删除本地文件](#2.上传文件后删除本地文件)
+ [3.下载文件并截断过长的文件名](#3.下载文件并截断过长的文件名)
+ [4.上传文件去掉文件名包含的部分字符](#4.上传文件去掉文件名包含的部分字符)
+ [5.Token刷新失败发送外部通知](#5.Token刷新失败发送外部通知)
* [显示和修改程序配置项](#显示和修改程序配置项)
- [常见问题Q&A](#常见问题Q&A)
* [1. 如何获取RefreshToken](#1-如何获取RefreshToken)
@@ -279,6 +282,25 @@ 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
@@ -1029,9 +1051,10 @@ server {
8.下载的文件路径进行更改,但是网盘的文件保持不变
9.下载文件完成后,通过HTTP通知其他服务
10.同步备份功能,支持过滤本地文件,或者过滤云盘文件。定制上传或者下载需要同步的文件
11.Token刷新失败或者过期,发送外部通知(HTTP&邮件)
### 如何使用
JS插件的样本文件默认存放在程序所在的plugin/js文件夹下,分为下载(download_handler.js.sample)、上传(upload_handler.js.sample)、同步备份(sync_handler.js.sample)插件。
JS插件的样本文件默认存放在程序所在的plugin/js文件夹下,分为下载(download_handler.js.sample)、上传(upload_handler.js.sample)、同步备份(sync_handler.js.sample)、用户Token(token_handler.js.sample)插件。
建议拷贝一份并将后缀名更改为.js,例如:upload_handler.js,不然插件不会生效。
你必须具备一定的JS语言基础,然后按照里面的样例根据自己所需进行改动即可。
@@ -1041,13 +1064,13 @@ JS插件的样本文件默认存放在程序所在的plugin/js文件夹下,分
目前只开放了如下函数,你可以在你的js脚本中直接调用
1.console.log()
打印日志
```
```js
console.log("hello world");
```
2.PluginUtil.Http.get()
发起HTTP的get请求
```
```js
var header = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36",
"Content-Type": "application/json",
@@ -1065,7 +1088,7 @@ console.log("hello world");
3.PluginUtil.Http.post()
发起HTTP的post请求
```
```js
var header = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36",
"Content-Type": "application/json",
@@ -1089,17 +1112,85 @@ console.log("hello world");
4.PluginUtil.LocalFS.deleteFile()
删除本地指定文件,不支持文件夹
```
```js
PluginUtil.LocalFS.deleteFile(params["localFilePath"]);
```
5.PluginUtil.Email.sendTextMail()
发送文本邮件,定义如下
```
PluginUtil.Email.sendTextMail(mailServer, userName, password, to, subject, body)
其中:
mailServer - smtp服务器+端口
userName - 发件人邮箱地址
password - 发件人邮箱密码
to - 收件人邮箱地址
subject - 邮件标题
body - 邮件内容,纯文本
```
样例
```js
PluginUtil.Email.sendTextMail("smtp.qq.com:465", "123xxx@qq.com", "pwdxxxxxx", "12545xxx@qq.com", "文件上传通知", "该文件已经上传完毕");
```
6.PluginUtil.Email.sendHtmlMail()
发送HTML富文本邮件,定义如下
```
PluginUtil.Email.sendHtmlMail(mailServer, userName, password, to, subject, body)
其中:
mailServer - smtp服务器+端口
userName - 发件人邮箱地址
password - 发件人邮箱密码
to - 收件人邮箱地址
subject - 邮件标题
body - 邮件内容,HTML富文本
```
样例
```js
var html = "<html>"
+"<body>"
+"<h1>文件上传通知</h1>"
+"<h2>该文件已经上传完毕</h2>"
+"</body>"
+"</html>";
PluginUtil.Email.sendHtmlMail("smtp.qq.com:465", "123xxx@qq.com", "pwdxxxxxx", "12545xxx@qq.com", "文件上传通知", html);
```
7.PluginUtil.KV.putString()
存储键值对,定义如下
```
PluginUtil.KV.putString(key, value)
其中:
key - 键,字符串
value - 值,字符串
```
样例
```js
PluginUtil.KV.putString("mykey", "1670419352");
```
8.PluginUtil.KV.getString()
获取存储的键值,指定对应的键,返回存储的值,如果没有则返回空字符串,定义如下
```
PluginUtil.KV.getString(key)
其中:
key - 键,字符串
```
样例
```js
var value = PluginUtil.KV.getString("mykey");
```
### 常见场景样例
这里收集了一些常见的需求样例,可以作为插件定制的样例模板。
#### 1.禁止特定文件上传
使用JavaScript上传插件中的`uploadFilePrepareCallback`函数。如下所示:
```
```js
function uploadFilePrepareCallback(context, params) {
console.log(params)
@@ -1133,7 +1224,7 @@ function uploadFilePrepareCallback(context, params) {
#### 2.上传文件后删除本地文件
使用JavaScript上传插件中的`uploadFileFinishCallback`函数。如下所示:
```
```js
function uploadFileFinishCallback(context, params) {
console.log(params);
if (params["localFileType"] != "file") {
@@ -1149,7 +1240,7 @@ function uploadFileFinishCallback(context, params) {
#### 3.下载文件并截断过长的文件名
有些文件的路径或者名称太长,下载的时候可能会由于路径名称过程导致无法下载,这时候可以使用JavaScript下载插件中的`downloadFilePrepareCallback`函数定制下载保存的文件名。
如下所示:
```
```js
function downloadFilePrepareCallback(context, params) {
console.log(params)
@@ -1215,7 +1306,87 @@ function downloadFilePrepareCallback(context, params) {
[1] 下载完成, 保存位置: D:\test\亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagra.mobi
[1] 检验文件有效性成功: D:\test\亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagra.mobi
```
#### 4.上传文件去掉文件名包含的部分字符
例如本地文件夹名称为```[周杰伦]范特西[mp3]```,上传到网盘希望能更改成名称```[周杰伦]范特西```但保持本地文件夹名称不变,因为文件夹有很多不希望每个手动更改,可以这样实现:
```js
function uploadFilePrepareCallback(context, params) {
var result = {
"uploadApproved": "yes",
"driveFilePath": ""
};
// 去掉网盘保存路径中包含的[mp3]字段
var filePath = params["driveFilePath"];
filePath = filePath.replace(/\[mp3\]/g, "");
result["driveFilePath"] = filePath;
return result;
}
```
#### 5.Token刷新失败发送外部通知
Token刷新失败发送外部通知,例如Server酱
```js
function userTokenRefreshFinishCallback(context, params) {
var header = {
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.88 Safari/537.36",
"Content-Type": "application/json",
"Accept": "application/json"
};
try {
if (params["result"] === "fail") {
// 避免频繁发送
var ONE_MINUTE = 60 * 1000;
var lastSendEmailTime = PluginUtil.KV.getString("email_last_send_time");
if (lastSendEmailTime != "") {
if ((Date.now() - lastSendEmailTime) < (10 * ONE_MINUTE)) {
console.log("距离上次发送邮件小于10分钟,先不发送了");
return
}
}
PluginUtil.KV.putString("email_last_send_time", Date.now());
var reqData = {
"text": "Token刷新失败",
"desp": params["message"]
};
var r = PluginUtil.Http.post(header, "https://sctapi.ftqq.com/xxxxxkeyxxxxxx.send", JSON.stringify(reqData));
}
} catch (e) {
if (e !== "Error") {
throw e;
}
}
}
```
或者发送邮件通知
```js
function userTokenRefreshFinishCallback(context, params) {
try {
if (params["result"] === "fail") {
// 避免频繁发送
var ONE_MINUTE = 60 * 1000;
var lastSendEmailTime = PluginUtil.KV.getString("email_last_send_time");
if (lastSendEmailTime != "") {
if ((Date.now() - lastSendEmailTime) < (10 * ONE_MINUTE)) {
console.log("距离上次发送邮件小于10分钟,先不发送了");
return
}
}
PluginUtil.KV.putString("email_last_send_time", Date.now());
// 发送通知邮件,请确保你的发送邮箱具备smtp发送权限,没有的需要找邮箱提供商配置开启
console.log("发送通知邮件");
PluginUtil.Email.sendTextMail("smtp.qq.com:465", "123xxx@qq.com", "pwdxxxxxx", "12545xxx@qq.com", "Token刷新失败了", "Token过期了,骚年快去手动恢复吧");
}
} catch (e) {
if (e !== "Error") {
throw e;
}
}
}
```
## 显示和修改程序配置项
```
+3 -2
View File
@@ -7,6 +7,7 @@ require (
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
@@ -14,9 +15,9 @@ require (
github.com/olekukonko/tablewriter v0.0.2-0.20190618033246-cc27d85e17ce
github.com/peterh/liner v1.2.1
github.com/satori/go.uuid v1.2.0
github.com/tickstep/aliyunpan-api v0.1.0
github.com/tickstep/aliyunpan-api v0.1.6
github.com/tickstep/bolt v1.3.3
github.com/tickstep/library-go v0.0.5
github.com/tickstep/library-go v0.1.0
github.com/urfave/cli v1.21.1-0.20190817182405-23c83030263f
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a
golang.org/x/net v0.0.0-20210614182718-04defd469f4e
+45 -15
View File
@@ -1,18 +1,34 @@
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
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 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
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=
@@ -21,22 +37,28 @@ github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRP
github.com/dop251/goja v0.0.0-20220408131256-ffe77e20c6f1 h1:9/4Hyp+A98nqssD7qceXwoLeAn75MZr25IN76dxlfbg=
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 h1:W1n4DvpzZGOISgp7wWNtraLcHtnmnTwBlJidqtMIuwQ=
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/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
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=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/pty v1.1.1 h1:VkoXIwSboBpnk99O/KFauAEILuNHv5DVFKZMBN/gUgw=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -56,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=
@@ -66,51 +92,55 @@ github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNue
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww=
github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749 h1:bUGsEnyNbVPw06Bs80sCeARAlK8lhwqGyi6UT8ymuGk=
github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd h1:ug7PpSOB5RBPK1Kg6qskGBoP3Vnj/aNYFTznWvlkGo0=
github.com/shurcooL/vfsgen v0.0.0-20181202132449-6a9ea43bcacd/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
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.0 h1:wTSCoBhQ1fBVJ9MGEDvrzdFR+uF8b6Ji2pDvY+/DL0g=
github.com/tickstep/aliyunpan-api v0.1.0/go.mod h1:ZFtMpbmVPM6+zWRTQ7QHw6xzwN/WTm3zaxQjFZ54UAA=
github.com/tickstep/aliyunpan-api v0.1.6 h1:wF35KWo1RpYNBWfdLLciuT6nlKvJiX4lhxAiGFUivRk=
github.com/tickstep/aliyunpan-api v0.1.6/go.mod h1:SN+N5Vh3lLq3dVimlxKxviCl50gLGXZO96nHHc3qd2o=
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.5 h1:MBb1tsvs4Wi67zy0E9eobVWLgsfPRLsqKAEdSEi3LBE=
github.com/tickstep/library-go v0.0.5/go.mod h1:egoK/RvOJ3Qs2tHpkq374CWjhNjI91JSCCG1GrhDYSw=
github.com/tickstep/library-go v0.1.0 h1:lzz6wpYCeliSzmVISa7rp9bGB4r+uIa9/VUTtk87DFU=
github.com/tickstep/library-go v0.1.0/go.mod h1:uAHeNOIpoywCzlaeLrWmmRSupn03m9kJVZKOEmuarmA=
github.com/urfave/cli v1.21.1-0.20190817182405-23c83030263f h1:xKDKjIsL76VUyHcA0G4Qe1cIAUB/nrq6Pt8D411bd1g=
github.com/urfave/cli v1.21.1-0.20190817182405-23c83030263f/go.mod h1:qXyCeJubPqsgeiLd3kvHOGHHSrQcNdjZ2ScXIcVZK/I=
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200115085410-6d4e4cb37c7d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a h1:kr2P4QFmQr29mSLA43kwrOcgcReGTfbE9N577tCTuBc=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E=
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=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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 h1:0vLT13EuvQ0hNvakwLuFZ/jYrLp5F3kcWHXdRggjCE8=
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=
+2 -2
View File
@@ -46,7 +46,7 @@ func CmdAlbum() cli.Command {
Usage: "相簿(Beta)",
UsageText: cmder.App().Name + " album",
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
@@ -429,7 +429,7 @@ func RunAlbumAddFile(albumName string, filePathList []string, filterOption Album
return
}
paths, err := matchPathByShellPattern(activeUser.ActiveDriveId, filePathList...)
paths, err := makePathAbsolute(activeUser.ActiveDriveId, filePathList...)
if err != nil {
fmt.Println(err)
return
+29 -8
View File
@@ -15,6 +15,7 @@ package command
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/urfave/cli"
@@ -33,14 +34,17 @@ func CmdCd() cli.Command {
切换 /我的资源 工作目录:
aliyunpan cd /我的资源
切换 /我的资源 工作目录,使用通配符:
aliyunpan cd /我的*
切换上级目录:
aliyunpan cd ..
切换根目录:
aliyunpan cd /
`,
Before: cmder.ReloadConfigFunc,
After: cmder.SaveConfigFunc,
Before: ReloadConfigFunc,
After: SaveConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() == 0 {
cli.ShowCommandHelp(c, c.Command.Name)
@@ -69,7 +73,7 @@ func CmdPwd() cli.Command {
Usage: "输出工作目录",
UsageText: cmder.App().Name + " pwd",
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
@@ -90,24 +94,41 @@ func RunChangeDirectory(driveId, targetPath string) {
user := config.Config.ActiveUser()
targetPath = user.PathJoin(driveId, targetPath)
targetPathInfo, err := user.PanClient().FileInfoByPath(driveId, targetPath)
//targetPathInfo, err := user.PanClient().FileInfoByPath(driveId, targetPath)
files, err := matchPathByShellPattern(driveId, targetPath)
if err != nil {
fmt.Println(err)
return
}
var targetPathInfo *aliyunpan.FileEntity
if len(files) == 1 {
targetPathInfo = files[0]
} else {
for _, f := range files {
if f.IsFolder() {
targetPathInfo = f
break
}
}
}
if targetPathInfo == nil {
fmt.Println("路径不存在")
return
}
if !targetPathInfo.IsFolder() {
fmt.Printf("错误: %s 不是一个目录 (文件夹)\n", targetPath)
fmt.Printf("错误: %s 不是一个目录 (文件夹)\n", targetPathInfo.Path)
return
}
if user.IsFileDriveActive() {
user.Workdir = targetPath
user.Workdir = targetPathInfo.Path
user.WorkdirFileEntity = *targetPathInfo
} else if user.IsAlbumDriveActive() {
user.AlbumWorkdir = targetPath
user.AlbumWorkdir = targetPathInfo.Path
user.AlbumWorkdirFileEntity = *targetPathInfo
}
fmt.Printf("改变工作目录: %s\n", targetPath)
fmt.Printf("改变工作目录: %s\n", targetPathInfo.Path)
}
+18 -6
View File
@@ -97,7 +97,7 @@ func newRapidUploadItem(rapidUploadShareLink string) (*RapidUploadItem, error) {
item.FileSha1 = strings.TrimSpace(parts[1])
// size
if size,e := strconv.ParseInt(parts[2], 10, 64); e == nil{
if size, e := strconv.ParseInt(parts[2], 10, 64); e == nil {
item.FileSize = size
} else {
return nil, fmt.Errorf("文件大小错误: %s", rapidUploadShareLink)
@@ -131,7 +131,6 @@ func (r *RapidUploadItem) createRapidUploadLink(hideRelativePath bool) string {
p := r.FilePath
p = strings.ReplaceAll(p, "\\", "/")
fileName := path.Base(p)
dirPath := path.Dir(p)
@@ -160,10 +159,10 @@ func CmdConfig() cli.Command {
Usage: "显示和修改程序配置项",
Description: "显示和修改程序配置项",
Category: "配置",
Before: cmder.ReloadConfigFunc,
After: cmder.SaveConfigFunc,
Before: ReloadConfigFunc,
After: SaveConfigFunc,
Action: func(c *cli.Context) error {
fmt.Printf("----\n运行 %s config set 可进行设置配置\n\n当前配置:\n", cmder.App().Name)
fmt.Printf("----\n当前配置目录: %s\n运行 %s config set 可进行设置配置\n\n当前配置:\n", config.GetConfigDir(), cmder.App().Name)
config.Config.PrintTable()
return nil
},
@@ -227,6 +226,12 @@ func CmdConfig() cli.Command {
if c.IsSet("local_addrs") {
config.Config.SetLocalAddrs(c.String("local_addrs"))
}
if c.IsSet("file_record_config") {
config.Config.SetFileRecorderConfig(c.String("file_record_config"))
}
if c.IsSet("device_id") {
config.Config.SetDeviceId(c.String("device_id"))
}
err := config.Config.Save()
if err != nil {
@@ -277,13 +282,20 @@ func CmdConfig() cli.Command {
Name: "local_addrs",
Usage: "设置本地网卡地址, 多个地址用逗号隔开",
},
cli.StringFlag{
Name: "file_record_config",
Usage: "设置是否开启上传、下载、同步文件的结果记录功能",
},
cli.StringFlag{
Name: "device_id",
Usage: "设置客户端ID,24位的字符串",
},
},
},
},
}
}
func CmdTool() cli.Command {
return cli.Command{
Name: "tool",
+103 -36
View File
@@ -20,6 +20,7 @@ import (
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/internal/file/downloader"
"github.com/tickstep/aliyunpan/internal/functions/pandownload"
"github.com/tickstep/aliyunpan/internal/log"
"github.com/tickstep/aliyunpan/internal/taskframework"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/aliyunpan/library/requester/transfer"
@@ -31,6 +32,7 @@ import (
"path"
"path/filepath"
"runtime"
"sync/atomic"
"time"
)
@@ -47,7 +49,8 @@ type (
NoCheck bool
ShowProgress bool
DriveId string
UseInternalUrl bool // 是否使用内置链接
UseInternalUrl bool // 是否使用内置链接
ExcludeNames []string // 排除的文件名,包括文件夹和文件。即这些文件/文件夹不进行下载,支持正则表达式
}
// LocateDownloadOption 获取下载链接可选参数
@@ -85,16 +88,30 @@ func CmdDownload() cli.Command {
aliyunpan config set -savedir D:/Downloads
下载 /我的资源/1.mp4
aliyunpan d /我的资源/1.mp4
aliyunpan download /我的资源/1.mp4
下载 /我的资源 目录下面所有的mp4文件,使用通配符
aliyunpan download /我的资源/*.mp4
下载 /我的资源 整个目录!!
aliyunpan d /我的资源
aliyunpan download /我的资源
下载 /我的资源/1.mp4 并保存下载的文件到本地的 d:/panfile
aliyunpan d --saveto d:/panfile /我的资源/1.mp4
下载 /我的资源 整个目录,但是排除所有的jpg文件
aliyunpan download -exn "\.jpg$" /我的资源
下载 /我的资源/1.mp4 并保存下载的文件到本地的 d:/panfile
aliyunpan download --saveto d:/panfile /我的资源/1.mp4
参考:
以下是典型的排除特定文件或者文件夹的例子,注意:参数值必须是正则表达式。在正则表达式中,^表示匹配开头,$表示匹配结尾。
1)排除@eadir文件或者文件夹:-exn "^@eadir$"
2)排除.jpg文件:-exn "\.jpg$"
3)排除.号开头的文件:-exn "^\."
4)排除~号开头的文件:-exn "^~"
5)排除 myfile.txt 文件:-exn "^myfile.txt$"
`,
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() == 0 {
cli.ShowCommandHelp(c, c.Command.Name)
@@ -124,9 +141,23 @@ func CmdDownload() cli.Command {
NoCheck: c.Bool("nocheck"),
ShowProgress: !c.Bool("np"),
DriveId: parseDriveId(c),
ExcludeNames: c.StringSlice("exn"),
}
// 获取下载文件锁,保证下载操作单实例
//locker := filelocker.NewFileLocker(config.GetLockerDir() + "/aliyunpan-download")
//if e := filelocker.LockFile(locker, 0755, true, 5*time.Second); e != nil {
// logger.Verboseln(e)
// fmt.Println("本应用其他实例正在执行下载,请先停止或者等待其完成")
// return nil
//}
RunDownload(c.Args(), do)
// 释放文件锁
//if locker != nil {
// filelocker.UnlockFile(locker)
//}
return nil
},
Flags: []cli.Flag{
@@ -172,6 +203,11 @@ func CmdDownload() cli.Command {
Usage: "网盘ID",
Value: "",
},
cli.StringSliceFlag{
Name: "exn",
Usage: "exclude name,指定排除的文件夹或者文件的名称,被排除的文件不会进行下载,只支持正则表达式。支持同时排除多个名称,每一个名称就是一个exn参数",
Value: nil,
},
},
}
}
@@ -190,14 +226,19 @@ func RunDownload(paths []string, options *DownloadOptions) {
activeUser.PanClient().ClearCache()
defer activeUser.PanClient().DisableCache()
// pan token expired checker
go func() {
for {
continueFlag := int32(0)
atomic.StoreInt32(&continueFlag, 0)
defer func() {
atomic.StoreInt32(&continueFlag, 1)
}()
go func(flag *int32) {
for atomic.LoadInt32(flag) == 0 {
time.Sleep(time.Duration(1) * time.Minute)
if RefreshTokenInNeed(activeUser) {
if RefreshTokenInNeed(activeUser, config.Config.DeviceName) {
logger.Verboseln("update access token for download task")
}
}
}()
}(&continueFlag)
if options == nil {
options = &DownloadOptions{}
@@ -221,6 +262,7 @@ func RunDownload(paths []string, options *DownloadOptions) {
InstanceStateStorageFormat: downloader.InstanceStateStorageFormatJSON,
ShowProgress: options.ShowProgress,
UseInternalUrl: config.Config.TransferUrlType == 2,
ExcludeNames: options.ExcludeNames,
}
if cfg.CacheSize == 0 {
cfg.CacheSize = int(DownloadCacheSize)
@@ -255,7 +297,7 @@ func RunDownload(paths []string, options *DownloadOptions) {
}
}
paths, err := matchPathByShellPattern(options.DriveId, paths...)
paths, err := makePathAbsolute(options.DriveId, paths...)
if err != nil {
fmt.Println(err)
return
@@ -280,36 +322,61 @@ func RunDownload(paths []string, options *DownloadOptions) {
// 全局速度统计
globalSpeedsStat := &speeds.Speeds{}
// 下载记录器
fileRecorder := log.NewFileRecorder(config.GetLogDir() + "/download_file_records.csv")
// 处理队列
for k := range paths {
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: paths[k],
DriveId: options.DriveId,
GlobalSpeedsStat: globalSpeedsStat,
// 使用通配符匹配
fileList, err2 := matchPathByShellPattern(options.DriveId, paths[k])
if err2 != nil {
fmt.Printf("获取文件出错,请稍后重试: %s\n", paths[k])
continue
}
if fileList == nil || len(fileList) == 0 {
// 文件不存在
fmt.Printf("文件不存在: %s\n", paths[k])
continue
}
for _, f := range fileList {
newCfg := *cfg
// 设置储存的路径
if options.SaveTo != "" {
unit.OriginSaveRootPath = options.SaveTo
unit.SavePath = filepath.Join(options.SaveTo, paths[k])
} else {
// 使用默认的保存路径
unit.OriginSaveRootPath = GetActiveUser().GetSavePath("")
unit.SavePath = GetActiveUser().GetSavePath(paths[k])
// 是否排除下载
if utils.IsExcludeFile(f.Path, &newCfg.ExcludeNames) {
fmt.Printf("排除文件: %s\n", f.Path)
continue
}
// 匹配的文件
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: options.DriveId,
GlobalSpeedsStat: globalSpeedsStat,
FileRecorder: fileRecorder,
}
// 设置储存的路径
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)
}
info := executor.Append(&unit, options.MaxRetry)
fmt.Printf("[%s] 加入下载队列: %s\n", info.Id(), paths[k])
}
// 开始计时
+2 -3
View File
@@ -17,7 +17,6 @@ import (
"fmt"
"github.com/olekukonko/tablewriter"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/cmder/cmdtable"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/urfave/cli"
@@ -38,8 +37,8 @@ func CmdDrive() cli.Command {
aliyunpan drive <driveId>
`,
Category: "阿里云盘账号",
Before: cmder.ReloadConfigFunc,
After: cmder.SaveConfigFunc,
Before: ReloadConfigFunc,
After: SaveConfigFunc,
Action: func(c *cli.Context) error {
inputData := c.Args().Get(0)
targetDriveId := strings.TrimSpace(inputData)
+4 -6
View File
@@ -28,7 +28,6 @@ import (
"time"
)
func CmdExport() cli.Command {
return cli.Command{
Name: "export",
@@ -50,7 +49,7 @@ func CmdExport() cli.Command {
aliyunpan export / /Users/tickstep/Downloads/export_files.txt
`,
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() < 2 {
cli.ShowCommandHelp(c, c.Command.Name)
@@ -75,12 +74,11 @@ func CmdExport() cli.Command {
}
}
func RunExportFiles(driveId string, overwrite bool, panPaths []string, saveLocalFilePath string) {
activeUser := config.Config.ActiveUser()
panClient := activeUser.PanClient()
lfi,_ := os.Stat(saveLocalFilePath)
lfi, _ := os.Stat(saveLocalFilePath)
realSaveFilePath := saveLocalFilePath
if lfi != nil {
if lfi.IsDir() {
@@ -94,7 +92,7 @@ func RunExportFiles(driveId string, overwrite bool, panPaths []string, saveLocal
} else {
// create file
localDir := path.Dir(saveLocalFilePath)
dirFs,_ := os.Stat(localDir)
dirFs, _ := os.Stat(localDir)
if dirFs != nil {
if !dirFs.IsDir() {
fmt.Println("指定的保存文件路径不合法")
@@ -117,7 +115,7 @@ func RunExportFiles(driveId string, overwrite bool, panPaths []string, saveLocal
return
}
for _,panPath := range panPaths {
for _, panPath := range panPaths {
panPath = activeUser.PathJoin(driveId, panPath)
panClient.FilesDirectoriesRecurseList(driveId, panPath, func(depth int, _ string, fd *aliyunpan.FileEntity, apiError *apierror.ApiError) bool {
if apiError != nil {
+1 -1
View File
@@ -64,7 +64,7 @@ func CmdImport() cli.Command {
aliyunpan import -saveto=/ /Users/tickstep/Downloads/export_files.txt
`,
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() < 1 {
cli.ShowCommandHelp(c, c.Command.Name)
+197
View File
@@ -0,0 +1,197 @@
// 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"
"github.com/tickstep/aliyunpan/cmder/cmdtable"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/library/collection"
"github.com/urfave/cli"
"io/ioutil"
"os"
"strconv"
"strings"
)
func CmdLocateUrl() cli.Command {
return cli.Command{
Name: "locate",
Usage: "获取文件下载链接",
UsageText: cmder.App().Name + " locate <文件/目录1> <文件/目录2> <文件/目录3> ...",
Description: `
获取文件下载直链,支持文件和文件夹。下载链接有效时间为4个小时。
导出的下载链接可以使用任何支持http下载的工具进行下载,如果是视频文件还可以使用支持在线流播放的视频软件进行在线播放。
注意:由于阿里云盘网页端有防盗链设置所以不能直接使用网页Token登录,你必须使用手机扫码二维码登录(命令:login -QrCode),否则获取的直链无法正常下载,会提示 403 Forbidden 下载被禁止。
示例:
获取 /我的资源/1.mp4 下载直链
aliyunpan locate /我的资源/1.mp4
获取 /我的资源 目录下面所有mp4文件的下载直链,使用通配符
aliyunpan locate /我的资源/*.mp4
获取 /我的资源 目录下面所有文件的下载直链并保存到本地文件 /Volumes/Downloads/file_url.txt 中
aliyunpan locate -saveto "/Volumes/Downloads/file_url.txt" /我的资源
`,
Category: "阿里云盘",
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() == 0 {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
}
saveFilePath := ""
if c.IsSet("saveto") {
saveFilePath = c.String("saveto")
}
RunLocateUrl(parseDriveId(c), c.Args(), saveFilePath)
return nil
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "driveId",
Usage: "网盘ID",
Value: "",
},
cli.StringFlag{
Name: "saveto",
Usage: "导出链接成文件并保存到指定的位置",
},
},
}
}
// RunLocateUrl 执行下载网盘内文件
func RunLocateUrl(driveId string, paths []string, saveFilePath string) {
useInternalUrl := config.Config.TransferUrlType == 2
activeUser := GetActiveUser()
activeUser.PanClient().EnableCache()
activeUser.PanClient().ClearCache()
defer activeUser.PanClient().DisableCache()
paths, err := makePathAbsolute(driveId, paths...)
if err != nil {
fmt.Println(err)
return
}
fileEntityQueue := collection.NewFifoQueue()
sb := &strings.Builder{}
failedList := []string{}
for _, p := range paths {
fileList, err1 := matchPathByShellPattern(driveId, p)
if err1 != nil {
failedList = append(failedList, p)
continue
}
if fileList == nil || len(fileList) == 0 {
// 文件不存在
failedList = append(failedList, p)
continue
}
for _, f := range fileList {
// 匹配的文件
fileEntityQueue.Push(f)
}
}
for {
item := fileEntityQueue.Pop()
if item == nil {
break
}
fileInfo := item.(*aliyunpan.FileEntity)
if fileInfo.IsFolder() { // 文件夹,获取下面所有文件
allFilesInFolder, er := activeUser.PanClient().FileListGetAll(&aliyunpan.FileListParam{
DriveId: driveId,
ParentFileId: fileInfo.FileId,
}, 300)
if er != nil {
failedList = append(failedList, fileInfo.Path)
continue
}
for _, f := range allFilesInFolder {
f.Path = fileInfo.Path + "/" + f.FileName
if f.IsFolder() {
// for next term
fileEntityQueue.Push(f)
continue
}
durl, apierr := activeUser.PanClient().GetFileDownloadUrl(&aliyunpan.GetFileDownloadUrlParam{
DriveId: driveId,
FileId: f.FileId,
})
if apierr != nil {
failedList = append(failedList, f.Path)
continue
}
url := durl.Url
if useInternalUrl {
url = durl.InternalUrl
}
if saveFilePath != "" {
fmt.Printf("获取文件下载链接:%s\n", f.Path)
fmt.Fprintf(sb, "\n文件:%s\n%s\n", f.Path, url)
} else {
fmt.Printf("\n文件:%s\n%s\n", f.Path, url)
}
}
} else { // 文件
durl, apierr := activeUser.PanClient().GetFileDownloadUrl(&aliyunpan.GetFileDownloadUrlParam{
DriveId: driveId,
FileId: fileInfo.FileId,
})
if apierr != nil {
failedList = append(failedList, fileInfo.Path)
continue
}
url := durl.Url
if useInternalUrl {
url = durl.InternalUrl
}
if saveFilePath != "" {
fmt.Printf("获取文件下载链接:%s\n", fileInfo.Path)
fmt.Fprintf(sb, "\n文件:%s\n%s\n", fileInfo.Path, url)
} else {
fmt.Printf("\n文件:%s\n%s\n", fileInfo.Path, url)
}
}
}
if saveFilePath != "" {
// save file
if e := ioutil.WriteFile(saveFilePath, []byte(sb.String()), 0777); e == nil {
fmt.Printf("保存文件成功:%s\n", saveFilePath)
} else {
fmt.Printf("保存文件失败:%s\n", saveFilePath)
}
}
// 输出失败的文件列表
if len(failedList) > 0 {
pnt := func() {
tb := cmdtable.NewTable(os.Stdout)
tb.SetHeader([]string{"#", "文件/目录"})
for k, f := range failedList {
tb.Append([]string{strconv.Itoa(k + 1), f})
}
tb.Render()
}
fmt.Printf("\n\n以下文件获取直链失败: \n")
pnt()
}
}
+17 -9
View File
@@ -16,7 +16,6 @@ package command
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/cmder/cmdliner"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/internal/functions/panlogin"
@@ -42,8 +41,8 @@ func CmdLogin() cli.Command {
aliyunpan login -QrCode
`,
Category: "阿里云盘账号",
Before: cmder.ReloadConfigFunc, // 每次进行登录动作的时候需要调用刷新配置
After: cmder.SaveConfigFunc, // 登录完成需要调用保存配置
Before: ReloadConfigFunc, // 每次进行登录动作的时候需要调用刷新配置
After: SaveConfigFunc, // 登录完成需要调用保存配置
Action: func(c *cli.Context) error {
refreshTokenStr := ""
if refreshTokenStr == "" {
@@ -61,7 +60,7 @@ func CmdLogin() cli.Command {
return err
}
cloudUser, err := config.SetupUserByCookie(&webToken)
cloudUser, err := config.SetupUserByCookie(&webToken, config.Config.DeviceId, config.Config.DeviceName)
if cloudUser == nil {
fmt.Println("登录失败: ", err)
return nil
@@ -93,8 +92,8 @@ func CmdLogout() cli.Command {
Usage: "退出阿里帐号",
Description: "退出当前登录的帐号",
Category: "阿里云盘账号",
Before: cmder.ReloadConfigFunc,
After: cmder.SaveConfigFunc,
Before: ReloadConfigFunc,
After: SaveConfigFunc,
Action: func(c *cli.Context) error {
if config.Config.NumLogins() == 0 {
fmt.Println("未设置任何帐号, 不能退出")
@@ -117,6 +116,13 @@ func CmdLogout() cli.Command {
}
}
// 云端注销登录
if _, e := activeUser.PanClient().DeviceLogout(); e != nil {
fmt.Printf("登出设备失败,请稍后重试: %s\n", e.String())
return nil
}
// 删除用户信息
deletedUser, err := config.Config.DeleteUser(activeUser.UserId)
if err != nil {
fmt.Printf("退出用户 %s, 失败, 错误: %s\n", activeUser.Nickname, err)
@@ -142,15 +148,16 @@ 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()
var qrCodeLoginResult *panlogin.QRCodeLoginResult
queryResult := true
defer line.Close()
go func() {
for {
for queryResult {
time.Sleep(3 * time.Second)
qr, er := h.GetQRCodeLoginResult(qrCodeUrlResult.TokenId)
if er != nil {
@@ -169,6 +176,7 @@ func RunLogin(useQrCodeLogin bool, refreshToken string) (tokenId, refreshTokenSt
line.State.Prompt("请在浏览器里面完成扫码登录,然后再按Enter键继续...")
if qrCodeLoginResult == nil {
queryResult = false
return "", "", aliyunpan.WebLoginToken{}, fmt.Errorf("二维码登录失败")
}
@@ -181,6 +189,6 @@ func RunLogin(useQrCodeLogin bool, refreshToken string) (tokenId, refreshTokenSt
tokenId = qrCodeUrlResult.TokenId
}
refreshTokenStr, webToken, error = cmder.DoLoginHelper(refreshToken)
refreshTokenStr, webToken, error = DoLoginHelper(refreshToken)
return
}
+26 -16
View File
@@ -21,7 +21,6 @@ import (
"github.com/tickstep/aliyunpan/cmder/cmdtable"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/library-go/converter"
"github.com/tickstep/library-go/text"
"github.com/urfave/cli"
"os"
"strconv"
@@ -62,11 +61,14 @@ func CmdLs() cli.Command {
绝对路径
aliyunpan ls /我的资源
列出 我的资源 内的文件和目录,使用通配符
aliyunpan ls /我的*
详细列出 我的资源 内的文件和目录
aliyunpan ll /我的资源
`,
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
@@ -138,15 +140,27 @@ func RunLs(driveId, targetPath string, lsOptions *LsOptions,
orderBy aliyunpan.FileOrderBy, orderDirection aliyunpan.FileOrderDirection) {
activeUser := config.Config.ActiveUser()
targetPath = activeUser.PathJoin(driveId, targetPath)
if targetPath[len(targetPath)-1] == '/' {
targetPath = text.Substr(targetPath, 0, len(targetPath)-1)
}
targetPathInfo, err := activeUser.PanClient().FileInfoByPath(driveId, targetPath)
files, err := matchPathByShellPattern(driveId, targetPath)
if err != nil {
fmt.Println(err)
return
}
var targetPathInfo *aliyunpan.FileEntity
if len(files) == 1 {
targetPathInfo = files[0]
} else {
for _, f := range files {
if f.IsFolder() {
targetPathInfo = f
break
}
}
}
if targetPathInfo == nil {
fmt.Println("目录路径不存在")
return
}
fileList := aliyunpan.FileList{}
fileListParam := &aliyunpan.FileListParam{}
@@ -155,16 +169,16 @@ func RunLs(driveId, targetPath string, lsOptions *LsOptions,
fileListParam.OrderBy = orderBy
fileListParam.OrderDirection = orderDirection
if targetPathInfo.IsFolder() {
fileResult, err := activeUser.PanClient().FileListGetAll(fileListParam, 0)
if err != nil {
fmt.Println(err)
fileResult, err1 := activeUser.PanClient().FileListGetAll(fileListParam, 200)
if err1 != nil {
fmt.Println(err1)
return
}
fileList = fileResult
} else {
fileList = append(fileList, targetPathInfo)
}
renderTable(opLs, lsOptions.Total, targetPath, fileList)
renderTable(opLs, lsOptions.Total, targetPathInfo.Path, fileList)
}
func renderTable(op int, isTotal bool, path string, files aliyunpan.FileList) {
@@ -218,12 +232,8 @@ func renderTable(op int, isTotal bool, path string, files aliyunpan.FileList) {
fN, dN = files.Count()
tb.Append([]string{"", "总: " + converter.ConvertFileSize(files.TotalSize(), 2), "", fmt.Sprintf("文件总数: %d, 目录总数: %d", fN, dN)})
}
fmt.Printf("\n当前目录: %s\n", path)
fmt.Printf("----\n")
tb.Render()
if fN+dN >= 60 {
fmt.Printf("\n当前目录: %s\n", path)
}
fmt.Printf("----\n")
}
+5 -8
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 {
@@ -29,7 +28,7 @@ func CmdMkdir() cli.Command {
Usage: "创建目录",
UsageText: cmder.App().Name + " mkdir <目录>",
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() == 0 {
cli.ShowCommandHelp(c, c.Command.Name)
@@ -44,8 +43,8 @@ func CmdMkdir() cli.Command {
},
Flags: []cli.Flag{
cli.StringFlag{
Name: "familyId",
Usage: "家庭云ID",
Name: "driveId",
Usage: "网盘ID",
Value: "",
},
},
@@ -55,11 +54,9 @@ func CmdMkdir() cli.Command {
func RunMkdir(driveId, name string) {
activeUser := GetActiveUser()
fullpath := activeUser.PathJoin(driveId, name)
pathSlice := strings.Split(fullpath, "/")
rs := &aliyunpan.MkdirResult{}
err := apierror.NewFailedApiError("")
rs, err = activeUser.PanClient().MkdirRecursive(driveId,"", "", 0, pathSlice)
rs, err = activeUser.PanClient().Mkdir(driveId, "", fullpath)
if err != nil {
fmt.Println("创建文件夹失败:" + err.Error())
@@ -74,4 +71,4 @@ func RunMkdir(driveId, name string) {
} else {
fmt.Println("创建文件夹失败: ", fullpath)
}
}
}
+43 -14
View File
@@ -16,10 +16,12 @@ package command
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/cmder/cmdtable"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/urfave/cli"
"os"
"path"
"strconv"
)
func CmdMv() cli.Command {
@@ -29,15 +31,18 @@ func CmdMv() cli.Command {
UsageText: `移动:
aliyunpan mv <文件/目录1> <文件/目录2> <文件/目录3> ... <目标目录>`,
Description: `
注意: 移动多个文件和目录时, 请确保每一个文件和目录都存在, 否则移动操作会失败.
注意: 移动多个文件和目录时, 请确保每一个文件和目录都存在, 否则移动操作会失败。支持通配符匹配移动文件,通配符当前只能匹配文件名,不能匹配文件路径。
示例:
将 /我的资源/1.mp4 移动到 根目录 /
aliyunpan mv /我的资源/1.mp4 /
将 /我的资源 目录下所有的.png文件 移动到 /我的图片 目录下面,使用通配符匹配
aliyunpan mv /我的资源/*.png /我的图片
`,
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() <= 1 {
cli.ShowCommandHelp(c, c.Command.Name)
@@ -47,7 +52,6 @@ func CmdMv() cli.Command {
fmt.Println("未登录账号")
return nil
}
RunMove(parseDriveId(c), c.Args()...)
return nil
},
@@ -66,7 +70,7 @@ func RunMove(driveId string, paths ...string) {
activeUser := GetActiveUser()
cacheCleanPaths := []string{}
opFileList, targetFile, _, err := getFileInfo(driveId, paths...)
if err != nil {
if err != nil {
fmt.Println(err)
return
}
@@ -83,20 +87,20 @@ func RunMove(driveId string, paths ...string) {
failedMoveFiles := []*aliyunpan.FileEntity{}
moveFileParamList := []*aliyunpan.FileMoveParam{}
fileId2FileEntity := map[string]*aliyunpan.FileEntity{}
for _,mfi := range opFileList {
for _, mfi := range opFileList {
fileId2FileEntity[mfi.FileId] = mfi
moveFileParamList = append(moveFileParamList,
&aliyunpan.FileMoveParam{
DriveId: driveId,
FileId: mfi.FileId,
ToDriveId: driveId,
DriveId: driveId,
FileId: mfi.FileId,
ToDriveId: driveId,
ToParentFileId: targetFile.FileId,
})
cacheCleanPaths = append(cacheCleanPaths, path.Dir(mfi.Path))
}
fmr,er := activeUser.PanClient().FileMove(moveFileParamList)
fmr, er := activeUser.PanClient().FileMove(moveFileParamList)
for _,rs := range fmr {
for _, rs := range fmr {
if !rs.Success {
failedMoveFiles = append(failedMoveFiles, fileId2FileEntity[rs.FileId])
}
@@ -104,13 +108,22 @@ func RunMove(driveId string, paths ...string) {
if len(failedMoveFiles) > 0 {
fmt.Println("以下文件移动失败:")
for _,f := range failedMoveFiles {
for _, f := range failedMoveFiles {
fmt.Println(f.FileName)
}
fmt.Println("")
}
if er == nil {
fmt.Println("操作成功, 已移动文件到目标目录: ", targetFile.Path)
pnt := func() {
tb := cmdtable.NewTable(os.Stdout)
tb.SetHeader([]string{"#", "文件/目录"})
for k, rs := range fmr {
tb.Append([]string{strconv.Itoa(k + 1), fileId2FileEntity[rs.FileId].Path})
}
tb.Render()
}
fmt.Println("操作成功, 以下文件已移动到目标目录: ", targetFile.Path)
pnt()
activeUser.DeleteCache(cacheCleanPaths)
} else {
fmt.Println("无法移动文件,请稍后重试")
@@ -130,6 +143,22 @@ func getFileInfo(driveId string, paths ...string) (opFileList []*aliyunpan.FileE
return nil, nil, nil, fmt.Errorf("指定目标文件夹不存在")
}
opFileList, failedPaths, error = GetAppFileInfoByPaths(driveId, paths[:len(paths)-1]...)
for idx := 0; idx < (len(paths) - 1); idx++ {
absolutePath = path.Clean(activeUser.PathJoin(driveId, paths[idx]))
fileList, err1 := matchPathByShellPattern(driveId, 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
}
+2 -3
View File
@@ -15,7 +15,6 @@ package command
import (
"fmt"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/library-go/converter"
"github.com/urfave/cli"
@@ -34,7 +33,7 @@ func CmdQuota() cli.Command {
Usage: "获取当前帐号空间配额",
Description: "获取网盘的总储存空间, 和已使用的储存空间",
Category: "阿里云盘账号",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
@@ -59,6 +58,6 @@ func RunGetQuotaInfo() (quotaInfo *QuotaInfo, error error) {
}
return &QuotaInfo{
UsedSize: int64(user.UsedSize),
Quota: int64(user.TotalSize),
Quota: int64(user.TotalSize),
}, nil
}
+1 -1
View File
@@ -45,7 +45,7 @@ func CmdRecycle() cli.Command {
aliyunpan recycle delete -all
`,
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NumFlags() <= 0 || c.NArg() <= 0 {
cli.ShowCommandHelp(c, c.Command.Name)
+178 -9
View File
@@ -15,14 +15,42 @@ package command
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apiutil"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/urfave/cli"
"path"
"regexp"
"sort"
"strconv"
"strings"
)
type (
fileItem struct {
file *aliyunpan.FileEntity
newFileName string
}
fileArray []*fileItem
)
func newFileItem(file *aliyunpan.FileEntity) *fileItem {
return &fileItem{
file: file,
newFileName: "",
}
}
func (x fileArray) Len() int {
return len(x)
}
func (x fileArray) Less(i, j int) bool {
return x[i].file.FileName < x[j].file.FileName
}
func (x fileArray) Swap(i, j int) {
x[i], x[j] = x[j], x[i]
}
func CmdRename() cli.Command {
return cli.Command{
Name: "rename",
@@ -31,18 +59,32 @@ func CmdRename() cli.Command {
aliyunpan rename <旧文件/目录名> <新文件/目录名>`,
Description: `
示例:
1. 将文件 1.mp4 重命名为 2.mp4
aliyunpan rename 1.mp4 2.mp4
将文件 1.mp4 重命名为 2.mp4
aliyunpan rename 1.mp4 2.mp4
2. 将文件 /test/1.mp4 重命名为 /test/2.mp4
要求必须是同一个文件目录内
aliyunpan rename /test/1.mp4 /test/2.mp4
将文件 /test/1.mp4 重命名为 /test/2.mp4
要求必须是同一个文件目录内
aliyunpan rename /test/1.mp4 /test/2.mp4
批量重命名,规则:rename [expression] [replacement] [file]
其中,
expression - 表达式,即文件名中需要去掉的部分,如果为*代表旧的文件名全部去掉
replacement - 需要替换的新文件名,如果包含 # 号会按顺序替换成数字编号
file - 文件匹配模式,指定需要查找的文件匹配字符串,支持通配符,例如:*.mp4 即当前目录下所有的mp4文件
3. 批量重命名,将当前目录下所有.mp4文件中包含的字符串 "我的视频" 全部改成 "视频"
aliyunpan rename 我的视频 视频 *.mp4
4. 批量重命名,将当前目录下所有.mp4文件全部进行 "视频+编号.mp4" 的重命名操作,旧的名称全部去掉,即:视频001.mp4, 视频002.mp4, 视频003.mp4...
aliyunpan rename * 视频###.mp4 *.mp4
5. 批量重命名,将当前目录下所有.mp4文件全部进行 "视频+编号.mp4" 的重命名操作,旧的名称全部去掉,直接重命名无需人工确认操作
aliyunpan rename -y * 视频###.mp4 *.mp4
`,
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() != 2 {
if c.NArg() != 2 && c.NArg() != 3 {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
}
@@ -50,7 +92,12 @@ func CmdRename() cli.Command {
fmt.Println("未登录账号")
return nil
}
RunRename(parseDriveId(c), c.Args().Get(0), c.Args().Get(1))
if c.NArg() == 2 {
RunRename(parseDriveId(c), c.Args().Get(0), c.Args().Get(1))
} else if c.NArg() == 3 {
// 批量重命名
RunRenameBatch(c.Bool("y"), parseDriveId(c), c.Args().Get(0), c.Args().Get(1), c.Args().Get(2))
}
return nil
},
Flags: []cli.Flag{
@@ -59,6 +106,10 @@ func CmdRename() cli.Command {
Usage: "网盘ID",
Value: "",
},
cli.BoolFlag{
Name: "y",
Usage: "跳过人工确认,对批量操作有效",
},
},
}
}
@@ -104,3 +155,121 @@ func RunRename(driveId string, oldName string, newName string) {
fmt.Printf("重命名文件成功:%s -> %s\n", path.Base(oldName), path.Base(newName))
activeUser.DeleteOneCache(path.Dir(newName))
}
// RunRenameBatch 批量重命名文件
func RunRenameBatch(skipConfirm bool, driveId string, expression, replacement, filePattern string) {
if len(expression) == 0 {
fmt.Println("旧文件名不能为空")
return
}
if !apiutil.CheckFileNameValid(replacement) {
fmt.Println("新文件名不能包含特殊字符:" + apiutil.FileNameSpecialChars)
return
}
if len(filePattern) == 0 {
fmt.Println("文件匹配模式不能为空")
return
}
if strings.ContainsAny(filePattern, "/") {
fmt.Println("文件匹配模式不能包含路径分隔符")
return
}
if strings.ContainsAny(filePattern, "\\") {
fmt.Println("文件匹配模式不能包含路径分隔符")
return
}
activeUser := GetActiveUser()
absolutePath := path.Clean(activeUser.PathJoin(driveId, filePattern))
fileList, err1 := matchPathByShellPattern(driveId, absolutePath)
if err1 != nil {
fmt.Println("查询文件出错:" + err1.Error())
return
}
if fileList == nil || len(fileList) == 0 {
fmt.Println("没有找到符合的文件")
return
}
// 文件列表按照名字排序
files := fileArray{}
for _, f := range fileList {
files = append(files, newFileItem(f))
}
if len(files) > 1 {
sort.Sort(files)
}
// 依次处理
var index = 1
for _, file := range files {
replacementStr := replaceNumStr(replacement, index)
index += 1
if expression == "*" {
// 旧的名称全部替换
file.newFileName = replacementStr
} else {
// 替换指定的名称片段
file.newFileName = strings.ReplaceAll(file.file.FileName, expression, replacementStr)
}
}
// 确认
if !skipConfirm {
fmt.Printf("以下文件将进行对应的重命名\n\n")
idx := 1
for _, file := range files {
fmt.Printf("%d) %s -> %s\n", idx, file.file.FileName, file.newFileName)
idx += 1
}
fmt.Printf("\n是否进行批量重命名,该操作不可逆(y/n): ")
confirm := ""
_, err := fmt.Scanln(&confirm)
if err != nil || (confirm != "y" && confirm != "Y") {
fmt.Println("用户取消了操作")
return
}
}
// 重命名
for _, file := range files {
b, e := activeUser.PanClient().FileRename(driveId, file.file.FileId, file.newFileName)
if e != nil {
fmt.Println(e.Err)
return
}
if !b {
fmt.Println("重命名文件失败")
return
}
fmt.Printf("重命名文件成功:%s -> %s\n", file.file.FileName, file.newFileName)
activeUser.DeleteOneCache(path.Dir(file.file.Path))
}
}
// replaceNumStr 将#替换成数字编号
func replaceNumStr(name string, num int) string {
pattern, _ := regexp.Compile("[#]+")
r := pattern.FindAllStringSubmatchIndex(name, -1)
if len(r) == 0 {
return name
}
newName := name
c := r[0]
return replaceNumStr(newName[:c[0]]+generateNumStr(num, c[1]-c[0])+newName[c[1]:], num)
}
func generateNumStr(num, count int) string {
s := strconv.Itoa(num)
return generateZeroStr(count-len(s)) + s
}
func generateZeroStr(count int) string {
if count <= 0 {
return ""
}
s := ""
for i := 0; i < count; i++ {
s += "0"
}
return s
}
+41
View File
@@ -0,0 +1,41 @@
package command
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"sort"
"testing"
)
func TestRenameSort(t *testing.T) {
files := fileArray{}
files = append(files, newFileItem(&aliyunpan.FileEntity{FileName: "0我的文件01.txt"}))
files = append(files, newFileItem(&aliyunpan.FileEntity{FileName: "4我的文件03.txt"}))
files = append(files, newFileItem(&aliyunpan.FileEntity{FileName: "3我的文件02.txt"}))
files = append(files, newFileItem(&aliyunpan.FileEntity{FileName: "1我的文件00.txt"}))
sort.Sort(files)
for _, f := range files {
fmt.Println(f.file.FileName)
}
}
func TestRenameNum0(t *testing.T) {
fmt.Println(replaceNumStr("我的文件###.txt", 2))
}
func TestRenameNum1(t *testing.T) {
fmt.Println(replaceNumStr("我的##文件###.txt", 123))
}
func TestRenameNum2(t *testing.T) {
fmt.Println(replaceNumStr("我的文件###.txt", 1233))
}
func TestRenameNum3(t *testing.T) {
fmt.Println(replaceNumStr("我的文件[###].txt", 1233))
}
func TestRenameNum4(t *testing.T) {
fmt.Println(replaceNumStr("我的文件.txt", 1233))
}
func TestRenameNum5(t *testing.T) {
fmt.Println(replaceNumStr("", 1233))
}
+21 -11
View File
@@ -32,7 +32,7 @@ func CmdRm() cli.Command {
UsageText: cmder.App().Name + " rm <文件/目录的路径1> <文件/目录2> <文件/目录3> ...",
Description: `
注意: 删除多个文件和目录时, 请确保每一个文件和目录都存在, 否则删除操作会失败.
被删除的文件或目录可在网盘文件回收站找回.
被删除的文件或目录可在网盘文件回收站找回。支持通配符匹配删除文件,通配符当前只能匹配文件名,不能匹配文件路径。
示例:
@@ -44,9 +44,12 @@ func CmdRm() cli.Command {
删除 /我的资源 整个目录 !!
aliyunpan rm /我的资源
删除 /我的资源 目录下面的所有.zip文件,使用通配符匹配
aliyunpan rm /我的资源/*.zip
`,
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() == 0 {
cli.ShowCommandHelp(c, c.Command.Name)
@@ -80,18 +83,25 @@ func RunRemove(driveId string, paths ...string) {
for _, p := range paths {
absolutePath := path.Clean(activeUser.PathJoin(driveId, p))
fe, err := activeUser.PanClient().FileInfoByPath(driveId, absolutePath)
if err != nil {
fileList, err1 := matchPathByShellPattern(driveId, absolutePath)
if err1 != nil {
failedRmPaths = append(failedRmPaths, absolutePath)
continue
}
fe.Path = absolutePath
delFileInfos = append(delFileInfos, &aliyunpan.FileBatchActionParam{
DriveId: driveId,
FileId: fe.FileId,
})
fileId2FileEntity[fe.FileId] = fe
cacheCleanDirs = append(cacheCleanDirs, path.Dir(fe.Path))
if fileList == nil || len(fileList) == 0 {
// 失败,文件不存在
failedRmPaths = append(failedRmPaths, absolutePath)
continue
}
for _, f := range fileList {
// 删除匹配的文件
delFileInfos = append(delFileInfos, &aliyunpan.FileBatchActionParam{
DriveId: driveId,
FileId: f.FileId,
})
fileId2FileEntity[f.FileId] = f
cacheCleanDirs = append(cacheCleanDirs, path.Dir(f.Path))
}
}
// delete
+25
View File
@@ -0,0 +1,25 @@
package command
import (
"fmt"
"testing"
)
func TestWildcard(t *testing.T) {
fmt.Println(isIncludeFile("ab0.txt", "ab0.txt"))
fmt.Println(isIncludeFile("*.zip", "aliyunpan-v0.0.1-darwin-macos-amd64.zip"))
fmt.Println(isIncludeFile(".*.swp", ".vd.txt.swp"))
fmt.Println(isIncludeFile("*.swp", ".swp"))
fmt.Println(isIncludeFile("*.swp", ".1swp"))
fmt.Println(isIncludeFile("a*b", "ab"))
fmt.Println(isIncludeFile("a*b", "aab"))
fmt.Println(isIncludeFile("a*b", "accccccccdb"))
fmt.Println(isIncludeFile("a?b", "acb"))
fmt.Println(isIncludeFile("a?b", "accb"))
fmt.Println(isIncludeFile("a[xyz]b", "axb"))
fmt.Println(isIncludeFile("ab[0-9].txt", "ab0.txt"))
fmt.Println(isIncludeFile("a*b/ab[0-9].txt", "acb/ab0.txt"))
fmt.Println(isIncludeFile("aliyunpan*", "aliyunpan-v0.0.1-darwin-macos-amd64[TNT].zip"))
}
+28 -7
View File
@@ -24,6 +24,7 @@ import (
"github.com/tickstep/library-go/logger"
"github.com/urfave/cli"
"os"
"path"
"path/filepath"
"strconv"
"time"
@@ -35,7 +36,7 @@ func CmdShare() cli.Command {
Usage: "分享文件/目录",
UsageText: cmder.App().Name + " share",
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
@@ -52,6 +53,9 @@ func CmdShare() cli.Command {
创建文件 1.mp4 的分享链接
aliyunpan share set 1.mp4
创建 /我的视频/ 目录下所有mp4文件的分享链接,支持通配符
aliyunpan share set /我的视频/*.mp4
创建文件 1.mp4 的分享链接,并指定分享密码为2333
aliyunpan share set -sharePwd 2333 1.mp4
@@ -235,15 +239,32 @@ func CmdShare() cli.Command {
// RunShareSet 执行分享
func RunShareSet(driveId string, paths []string, expiredTime string, sharePwd string) {
panClient := GetActivePanClient()
fileList, _, err := GetFileInfoByPaths(paths[:len(paths)]...)
if err != nil {
fmt.Println(err)
if len(paths) <= 0 {
fmt.Println("请指定文件路径")
return
}
activeUser := GetActiveUser()
panClient := activeUser.PanClient()
allFileList := []*aliyunpan.FileEntity{}
for idx := 0; idx < len(paths); idx++ {
absolutePath := path.Clean(activeUser.PathJoin(driveId, paths[idx]))
fileList, err1 := matchPathByShellPattern(driveId, absolutePath)
if err1 != nil {
fmt.Println("文件不存在: " + absolutePath)
continue
}
if fileList == nil || len(fileList) == 0 {
// 文件不存在
fmt.Println("文件不存在: " + absolutePath)
continue
}
// 匹配的文件
allFileList = append(allFileList, fileList...)
}
fidList := []string{}
for _, f := range fileList {
for _, f := range allFileList {
fidList = append(fidList, f.FileId)
}
@@ -263,7 +284,7 @@ func RunShareSet(driveId string, paths []string, expiredTime string, sharePwd st
if err1.Code == apierror.ApiCodeFileShareNotAllowed {
fmt.Printf("创建分享链接失败: 该文件类型不允许分享\n")
} else {
fmt.Printf("创建分享链接失败: %s\n", err)
fmt.Printf("创建分享链接失败: %s\n", err1)
}
return
}
+32 -6
View File
@@ -18,6 +18,7 @@ import (
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/internal/log"
"github.com/tickstep/aliyunpan/internal/syncdrive"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/library-go/converter"
@@ -26,6 +27,7 @@ import (
"os"
"path"
"strings"
"sync/atomic"
"time"
)
@@ -50,7 +52,7 @@ func CmdSync() cli.Command {
aliyunpan sync start -h
`,
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
@@ -214,9 +216,23 @@ priority - 优先级,只对双向同步备份模式有效。选项支持三种
task.Name = path.Base(task.LocalFolderPath)
task.Id = utils.Md5Str(task.LocalFolderPath)
task.Priority = syncOpt
task.UserId = activeUser.UserId
}
// 获取同步文件锁,保证同步操作单实例
//locker := filelocker.NewFileLocker(config.GetLockerDir() + "/aliyunpan-sync")
//if e := filelocker.LockFile(locker, 0755, true, 5*time.Second); e != nil {
// logger.Verboseln(e)
// fmt.Println("本应用其他实例正在执行同步,请先停止或者等待其完成")
// return nil
//}
RunSync(task, dp, up, downloadBlockSize, uploadBlockSize, syncOpt, c.Int("ldt"), step)
// 释放文件锁
//if locker != nil {
// filelocker.UnlockFile(locker)
//}
return nil
},
Flags: []cli.Flag{
@@ -255,7 +271,7 @@ priority - 优先级,只对双向同步备份模式有效。选项支持三种
},
cli.IntFlag{
Name: "ubs",
Usage: "upload block size,上传分片大小,单位KB。推荐值:1024 ~ 10240",
Usage: "upload block size,上传分片大小,单位KB。推荐值:1024 ~ 10240。当上传极大单文件时候请适当调高该值",
Value: 10240,
},
cli.StringFlag{
@@ -289,15 +305,20 @@ func RunSync(defaultTask *syncdrive.SyncTask, fileDownloadParallel, fileUploadPa
panClient.DisableCache()
// pan token expired checker
go func() {
for {
continueFlag := int32(0)
atomic.StoreInt32(&continueFlag, 0)
defer func() {
atomic.StoreInt32(&continueFlag, 1)
}()
go func(flag *int32) {
for atomic.LoadInt32(flag) == 0 {
time.Sleep(time.Duration(1) * time.Minute)
if RefreshTokenInNeed(activeUser) {
if RefreshTokenInNeed(activeUser, config.Config.DeviceName) {
logger.Verboseln("update access token for sync task")
panClient.UpdateToken(activeUser.WebToken)
}
}
}()
}(&continueFlag)
syncFolderRootPath := config.GetSyncDriveDir()
if b, e := utils.PathExists(syncFolderRootPath); e == nil {
@@ -317,6 +338,10 @@ func RunSync(defaultTask *syncdrive.SyncTask, fileDownloadParallel, fileUploadPa
if useInternalUrl {
typeUrlStr = "阿里ECS内部链接"
}
// 文件同步记录器
fileRecorder := log.NewFileRecorder(config.GetLogDir() + "/sync_file_records.csv")
option := syncdrive.SyncOption{
FileDownloadParallel: fileDownloadParallel,
FileUploadParallel: fileUploadParallel,
@@ -327,6 +352,7 @@ func RunSync(defaultTask *syncdrive.SyncTask, fileDownloadParallel, fileUploadPa
MaxUploadRate: maxUploadRate,
SyncPriority: flag,
LocalFileModifiedCheckIntervalSec: localDelayTime,
FileRecorder: fileRecorder,
}
syncMgr := syncdrive.NewSyncTaskManager(activeUser, activeUser.DriveList.GetFileDriveId(), panClient, syncFolderRootPath, option)
syncConfigFile := syncMgr.ConfigFilePath()
+35 -5
View File
@@ -18,6 +18,9 @@ import (
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/internal/plugins"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/library-go/logger"
"github.com/urfave/cli"
)
@@ -27,7 +30,7 @@ func CmdToken() cli.Command {
Usage: "Token相关操作",
UsageText: cmder.App().Name + " token",
Category: "阿里云盘账号",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
cli.ShowCommandHelp(c, c.Command.Name)
return nil
@@ -73,27 +76,54 @@ func CmdToken() cli.Command {
// RunTokenUpdate 执行Token更新
func RunTokenUpdate(modeFlag string) {
cmder.ReloadConfigFunc(nil)
ReloadConfigFunc(nil)
// 获取当前插件
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
plugin, _ := pluginManger.GetPlugin()
params := &plugins.UserTokenRefreshFinishParams{
Result: "success",
Message: "",
OldToken: "",
NewToken: "",
UpdatedAt: utils.NowTimeStr(),
}
userList := config.Config.UserList
if userList == nil || len(userList) == 0 {
fmt.Printf("没有登录用户,无需刷新Token\n")
return
}
for _,user := range userList {
for _, user := range userList {
params.Result = "success"
if modeFlag == "1" {
if user.UserId != config.Config.ActiveUID {
continue
}
}
newToken,e := aliyunpan.GetAccessTokenFromRefreshToken(user.RefreshToken)
newToken, e := aliyunpan.GetAccessTokenFromRefreshToken(user.RefreshToken)
if e != nil {
params.Result = "fail"
params.Message = e.Error()
fmt.Printf("无法为%s用户获取新的RefreshToken,可能需要重新登录\n", user.Nickname)
continue
}
if newToken != nil && newToken.RefreshToken != "" {
params.OldToken = user.RefreshToken
params.NewToken = newToken.RefreshToken
user.RefreshToken = newToken.RefreshToken
user.WebToken = *newToken
fmt.Printf("成功刷新%s用户的RefreshToken\n", user.Nickname)
} else {
params.Result = "fail"
}
// plugin callback
if er := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(user), params); er != nil {
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er.Error())
}
}
cmder.SaveConfigFunc(nil)
SaveConfigFunc(nil)
}
+110 -18
View File
@@ -5,6 +5,7 @@ import (
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/library-go/converter"
"github.com/urfave/cli"
"path"
"strings"
@@ -26,17 +27,38 @@ func CmdTree() cli.Command {
列出 /我的资源 内的文件和目录的树形图
aliyunpan tree /我的资源
列出 /我的资源 内的文件和目录的树形图,使用通配符
aliyunpan tree /我的*
列出 /我的资源 内的文件和目录的树形图,并且显示文件对应的完整绝对路径
aliyunpan tree -fp /我的资源
列出 /我的资源 内的文件和目录的树形图,并且显示文件对应的文件大小
aliyunpan tree -fs /我的资源
列出 /我的资源 内的文件和目录的树形图,过滤大于等于 10kb 并且小于等于 10mb 的文件,同时显示文件对应的文件大小
aliyunpan tree -fs -minSize=1kb -maxSize=10mb /我的资源
`,
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
return nil
}
RunTree(parseDriveId(c), c.Args().Get(0), c.Bool("fp"))
minSize := int64(0)
if c.IsSet("minSize") {
if s, e := converter.ParseFileSizeStr(c.String("minSize")); e == nil {
minSize = s
}
}
maxSize := int64(0)
if c.IsSet("maxSize") {
if s, e := converter.ParseFileSizeStr(c.String("maxSize")); e == nil {
maxSize = s
}
}
RunTree(parseDriveId(c), c.Args().Get(0), c.Bool("fp"), c.Bool("fs"), minSize, maxSize)
return nil
},
Flags: []cli.Flag{
@@ -47,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",
},
},
}
@@ -63,19 +97,48 @@ type (
treeStatistic struct {
CountOfDir int64
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)
targetPathInfo, err1 := activeUser.PanClient().FileInfoByPath(driveId, pathStr)
if err1 != nil {
fmt.Println(err1)
files, err := matchPathByShellPattern(driveId, pathStr)
if err != nil {
fmt.Println(err)
return
}
var targetPathInfo *aliyunpan.FileEntity
if len(files) == 1 {
targetPathInfo = files[0]
} else {
for _, f := range files {
if f.IsFolder() {
targetPathInfo = f
break
}
}
}
if targetPathInfo == nil {
fmt.Println("路径不存在")
return
}
if depth == 0 {
fmt.Printf("%s\n", targetPathInfo.Path)
}
fileList := aliyunpan.FileList{}
fileListParam := &aliyunpan.FileListParam{}
fileListParam.ParentFileId = targetPathInfo.FileId
@@ -83,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, 0)
fileResult, err := activeUser.PanClient().FileListGetAll(fileListParam, 500)
if err != nil {
fmt.Println(err)
return
@@ -101,24 +164,47 @@ func getTree(driveId, pathStr string, depth int, statistic *treeStatistic, showF
for i, file := range fileList {
if file.IsFolder() {
statistic.CountOfDir += 1
if showFullPath {
fmt.Printf("%v%v %v/ -> %s\n", indentPrefixStr, pathPrefix, file.FileName, pathStr+"/"+file.FileName)
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, pathStr+"/"+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
if i+1 == fN {
prefix = lastFilePrefix
}
if showFullPath {
fmt.Printf("%v%v %v -> %s\n", indentPrefixStr, prefix, file.FileName, pathStr+"/"+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())
}
}
@@ -126,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()
@@ -135,8 +221,14 @@ func RunTree(driveId, pathStr string, showFullPath bool) {
statistic := &treeStatistic{
CountOfDir: 0,
CountOfFile: 0,
SizeOfFile: 0,
}
fmt.Printf("%s\n", pathStr)
getTree(driveId, pathStr, 0, statistic, showFullPath)
fmt.Printf("\n%d 个文件夹, %d 个文件\n", statistic.CountOfDir, statistic.CountOfFile)
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))
}
+63 -31
View File
@@ -17,15 +17,16 @@ import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
"github.com/tickstep/aliyunpan/cmder"
"github.com/tickstep/aliyunpan/internal/log"
"github.com/tickstep/aliyunpan/internal/plugins"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/library-go/requester/rio/speeds"
"os"
"path"
"path/filepath"
"regexp"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/tickstep/library-go/logger"
@@ -57,6 +58,7 @@ type (
AllParallel int // 所有文件并发上传数量,即可以同时并发上传多少个文件
Parallel int // 单个文件并发上传数量
MaxRetry int
MaxTimeoutSec int // http请求超时时间,单位秒
NoRapidUpload bool
ShowProgress bool
IsOverwrite bool // 覆盖已存在的文件,如果同名文件已存在则移到回收站里
@@ -78,6 +80,10 @@ var UploadFlags = []cli.Flag{
Usage: "上传失败最大重试次数",
Value: DefaultUploadMaxRetry,
},
cli.IntFlag{
Name: "timeout",
Usage: "上传请求超时时间,单位为秒。当遇到网络不好导致上传超时可以尝试调大该值,建议设置30秒以上",
},
cli.BoolFlag{
Name: "np",
Usage: "no progress 不展示上传进度条",
@@ -102,7 +108,7 @@ var UploadFlags = []cli.Flag{
},
cli.IntFlag{
Name: "bs",
Usage: "block size,上传分片大小,单位KB。推荐值:1024 ~ 10240",
Usage: "block size,上传分片大小,单位KB。推荐值:1024 ~ 10240。当上传极大单文件时候请适当调高该值",
Value: 10240,
},
}
@@ -127,19 +133,22 @@ func CmdUpload() cli.Command {
3. 将本地的 C:\Users\Administrator\Desktop 整个目录上传到网盘 /视频 目录
aliyunpan upload C:/Users/Administrator/Desktop /视频
4. 使用相对路径
4. 将本地 200GB 极大文件 C:\Users\Administrator\Desktop\1.mp4 上传到网盘 /视频 目录,需要调高上传分片大小
aliyunpan upload -bs 30720 C:/Users/Administrator/Desktop/1.mp4 /视频
5. 使用相对路径
aliyunpan upload 1.mp4 /视频
5. 覆盖上传,已存在的同名文件会被移到回收站
6. 覆盖上传,已存在的同名文件会被移到回收站
aliyunpan upload -ow 1.mp4 /视频
6. 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的.jpg文件
7. 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的.jpg文件
aliyunpan upload -exn "\.jpg$" C:/Users/Administrator/Video /视频
7. 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的.jpg文件和.mp3文件,每一个排除项就是一个exn参数
8. 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的.jpg文件和.mp3文件,每一个排除项就是一个exn参数
aliyunpan upload -exn "\.jpg$" -exn "\.mp3$" C:/Users/Administrator/Video /视频
8. 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的 @eadir 文件夹
9. 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的 @eadir 文件夹
aliyunpan upload -exn "^@eadir$" C:/Users/Administrator/Video /视频
参考:
@@ -151,7 +160,7 @@ func CmdUpload() cli.Command {
5)排除 myfile.txt 文件:-exn "^myfile.txt$"
`,
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() < 2 {
cli.ShowCommandHelp(c, c.Command.Name)
@@ -159,10 +168,28 @@ func CmdUpload() cli.Command {
}
subArgs := c.Args()
timeout := 0
if c.IsSet("timeout") {
timeout = c.Int("timeout")
if timeout < 0 {
timeout = 0
}
}
// 获取上传文件锁,保证上传操作单实例
//locker := filelocker.NewFileLocker(config.GetLockerDir() + "/aliyunpan-upload")
//if e := filelocker.LockFile(locker, 0755, true, 5*time.Second); e != nil {
// logger.Verboseln(e)
// fmt.Println("本应用其他实例正在执行上传,请先停止或者等待其完成")
// return nil
//}
RunUpload(subArgs[:c.NArg()-1], subArgs[c.NArg()-1], &UploadOptions{
AllParallel: c.Int("p"), // 多文件上传的时候,允许同时并行上传的文件数量
Parallel: 1, // 一个文件同时多少个线程并发上传的数量。阿里云盘只支持单线程按顺序进行文件part数据上传,所以只能是1
MaxRetry: c.Int("retry"),
MaxTimeoutSec: timeout,
NoRapidUpload: c.Bool("norapid"),
ShowProgress: !c.Bool("np"),
IsOverwrite: c.Bool("ow"),
@@ -170,6 +197,11 @@ func CmdUpload() cli.Command {
ExcludeNames: c.StringSlice("exn"),
BlockSize: int64(c.Int("bs") * 1024),
})
// 释放文件锁
//if locker != nil {
// filelocker.UnlockFile(locker)
//}
return nil
},
Flags: UploadFlags,
@@ -200,7 +232,7 @@ func CmdRapidUpload() cli.Command {
aliyunpan rapidupload "aliyunpan://file.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|pan_folder" "aliyunpan://file1.dmg|752FCCBFB2436A6FFCA3B287831D4FAA5654B07E|7005440|pan_folder"
`,
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() <= 0 {
cli.ShowCommandHelp(c, c.Command.Name)
@@ -236,14 +268,19 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
activeUser.PanClient().ClearCache()
defer activeUser.PanClient().DisableCache()
// pan token expired checker
go func() {
for {
continueFlag := int32(0)
atomic.StoreInt32(&continueFlag, 0)
defer func() {
atomic.StoreInt32(&continueFlag, 1)
}()
go func(flag *int32) {
for atomic.LoadInt32(flag) == 0 {
time.Sleep(time.Duration(1) * time.Minute)
if RefreshTokenInNeed(activeUser) {
if RefreshTokenInNeed(activeUser, config.Config.DeviceName) {
logger.Verboseln("update access token for upload task")
}
}
}()
}(&continueFlag)
if opt == nil {
opt = &UploadOptions{}
@@ -268,6 +305,11 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
}
opt.UseInternalUrl = config.Config.TransferUrlType == 2
// 超时时间
if opt.MaxTimeoutSec > 0 {
activeUser.PanClient().SetTimeout(time.Duration(opt.MaxTimeoutSec) * time.Second)
}
fmt.Printf("\n[0] 当前文件上传最大并发量为: %d, 上传分片大小为: %s\n", opt.AllParallel, converter.ConvertFileSize(opt.BlockSize, 2))
savePath = activeUser.PathJoin(opt.DriveId, savePath)
@@ -311,6 +353,9 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
// 获取当前插件
plugin, _ := pluginManger.GetPlugin()
// 上传记录器
fileRecorder := log.NewFileRecorder(config.GetLogDir() + "/upload_file_records.csv")
// 遍历指定的文件并创建上传任务
for _, curPath := range localPaths {
var walkFunc localfile.MyWalkFunc
@@ -318,7 +363,7 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
localPathDir := filepath.Dir(curPath)
// 是否排除上传
if isExcludeFile(curPath, opt) {
if utils.IsExcludeFile(curPath, &opt.ExcludeNames) {
fmt.Printf("排除文件: %s\n", curPath)
continue
}
@@ -340,7 +385,7 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
}
// 是否排除上传
if isExcludeFile(file.LogicPath, opt) {
if utils.IsExcludeFile(file.LogicPath, &opt.ExcludeNames) {
fmt.Printf("排除文件: %s\n", file.LogicPath)
return filepath.SkipDir
}
@@ -398,6 +443,7 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
IsOverwrite: opt.IsOverwrite,
UseInternalUrl: opt.UseInternalUrl,
GlobalSpeedsStat: globalSpeedsStat,
FileRecorder: fileRecorder,
}, opt.MaxRetry)
fmt.Printf("[%s] 加入上传队列: %s\n", taskinfo.Id(), file.LogicPath)
} else {
@@ -405,6 +451,7 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
// 这样空文件夹也可以正确上传
saveFilePath := subSavePath
if saveFilePath != "/" {
folderCreateMutex.Lock()
fmt.Printf("正在检测和创建云盘文件夹: %s\n", saveFilePath)
_, apierr1 := activeUser.PanClient().FileInfoByPath(opt.DriveId, saveFilePath)
time.Sleep(1 * time.Second)
@@ -415,6 +462,7 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
fmt.Printf("创建云盘文件夹失败: %s\n", saveFilePath)
}
}
folderCreateMutex.Unlock()
}
}
return nil
@@ -454,22 +502,6 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
activeUser.DeleteCache(GetAllPathFolderByPath(savePath))
}
// 是否是排除上传的文件
func isExcludeFile(filePath string, opt *UploadOptions) bool {
if opt == nil || len(opt.ExcludeNames) == 0 {
return false
}
for _, pattern := range opt.ExcludeNames {
fileName := path.Base(strings.ReplaceAll(filePath, "\\", "/"))
m, _ := regexp.MatchString(pattern, fileName)
if m {
return true
}
}
return false
}
// RunRapidUpload 秒传
func RunRapidUpload(driveId string, isOverwrite bool, fileMetaList []string, savePanPath string) {
activeUser := GetActiveUser()
+5 -5
View File
@@ -29,7 +29,7 @@ func CmdLoglist() cli.Command {
Usage: "列出帐号列表",
Description: "列出所有已登录的阿里账号",
Category: "阿里云盘账号",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
fmt.Println(config.Config.UserList.String())
return nil
@@ -50,8 +50,8 @@ func CmdSu() cli.Command {
aliyunpan su <uid or name>
`,
Category: "阿里云盘账号",
Before: cmder.ReloadConfigFunc,
After: cmder.SaveConfigFunc,
Before: ReloadConfigFunc,
After: SaveConfigFunc,
Action: func(c *cli.Context) error {
if c.NArg() >= 2 {
cli.ShowCommandHelp(c, c.Command.Name)
@@ -102,7 +102,7 @@ func CmdSu() cli.Command {
}
if switchedUser == nil {
switchedUser = cmder.TryLogin()
switchedUser = TryLogin()
}
if switchedUser != nil {
@@ -122,7 +122,7 @@ func CmdWho() cli.Command {
Usage: "获取当前帐号",
Description: "获取当前帐号的信息",
Category: "阿里云盘账号",
Before: cmder.ReloadConfigFunc,
Before: ReloadConfigFunc,
Action: func(c *cli.Context) error {
if config.Config.ActiveUser() == nil {
fmt.Println("未登录账号")
+201 -48
View File
@@ -16,58 +16,76 @@ package command
import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
"github.com/tickstep/aliyunpan/cmder/cmdliner"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/internal/functions/panlogin"
"github.com/tickstep/aliyunpan/internal/plugins"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/library-go/logger"
"github.com/urfave/cli"
"math/rand"
"net/url"
"path"
"path/filepath"
"strings"
"sync"
"time"
)
var (
panCommandVerbose = logger.New("PANCOMMAND", config.EnvVerbose)
saveConfigMutex *sync.Mutex = new(sync.Mutex)
ReloadConfigFunc = func(c *cli.Context) error {
err := config.Config.Reload()
if err != nil {
fmt.Printf("重载配置错误: %s\n", err)
}
return nil
}
SaveConfigFunc = func(c *cli.Context) error {
saveConfigMutex.Lock()
defer saveConfigMutex.Unlock()
err := config.Config.Save()
if err != nil {
fmt.Printf("保存配置错误: %s\n", err)
}
return nil
}
)
// GetFileInfoByPaths 获取指定文件路径的文件详情信息
func GetAppFileInfoByPaths(driveId string, paths ...string) (fileInfoList []*aliyunpan.FileEntity, failedPaths []string, error error) {
if len(paths) <= 0 {
return nil, nil, fmt.Errorf("请指定文件路径")
// RunTestShellPattern 执行测试通配符
func RunTestShellPattern(driveId string, pattern string) {
acUser := GetActiveUser()
files, err := acUser.PanClient().MatchPathByShellPattern(driveId, GetActiveUser().PathJoin(driveId, pattern))
if err != nil {
fmt.Println(err)
return
}
activeUser := GetActiveUser()
for idx := 0; idx < len(paths); idx++ {
absolutePath := path.Clean(activeUser.PathJoin(driveId, paths[idx]))
fe, err := activeUser.PanClient().FileInfoByPath(driveId, absolutePath)
if err != nil {
failedPaths = append(failedPaths, absolutePath)
continue
}
fileInfoList = append(fileInfoList, fe)
for _, f := range *files {
fmt.Printf("%s\n", f.Path)
}
return
}
// GetFileInfoByPaths 获取指定文件路径的文件详情信息
func GetFileInfoByPaths(paths ...string) (fileInfoList []*aliyunpan.FileEntity, failedPaths []string, error error) {
if len(paths) <= 0 {
return nil, nil, fmt.Errorf("请指定文件路径")
}
activeUser := GetActiveUser()
for idx := 0; idx < len(paths); idx++ {
absolutePath := path.Clean(activeUser.PathJoin(activeUser.ActiveDriveId, paths[idx]))
fe, err := activeUser.PanClient().FileInfoByPath(activeUser.ActiveDriveId, absolutePath)
// matchPathByShellPattern 通配符匹配路径,允许返回多个匹配结果
func matchPathByShellPattern(driveId string, patterns ...string) (files []*aliyunpan.FileEntity, e error) {
acUser := GetActiveUser()
for k := range patterns {
ps, err := acUser.PanClient().MatchPathByShellPattern(driveId, acUser.PathJoin(driveId, patterns[k]))
if err != nil {
failedPaths = append(failedPaths, absolutePath)
continue
return nil, err
}
fileInfoList = append(fileInfoList, fe)
files = append(files, *ps...)
}
return
return files, nil
}
func matchPathByShellPattern(driveId string, patterns ...string) (panpaths []string, err error) {
// makePathAbsolute 拼接路径,确定返回路径为绝对路径
func makePathAbsolute(driveId string, patterns ...string) (panpaths []string, err error) {
acUser := GetActiveUser()
for k := range patterns {
ps := acUser.PathJoin(driveId, patterns[k])
@@ -111,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
}
@@ -123,13 +141,50 @@ func RefreshTokenInNeed(activeUser *config.PanUser) bool {
expiredTime, _ := time.ParseInLocation("2006-01-02 15:04:05", activeUser.WebToken.ExpireTime, cz)
now := time.Now()
if (expiredTime.Unix() - now.Unix()) <= (20 * 60) { // 20min
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
plugin, _ := pluginManger.GetPlugin()
params := &plugins.UserTokenRefreshFinishParams{
Result: "success",
Message: "",
OldToken: "",
NewToken: "",
UpdatedAt: utils.NowTimeStr(),
}
// need update refresh token
logger.Verboseln("access token expired, get new from refresh token")
if wt, er := aliyunpan.GetAccessTokenFromRefreshToken(activeUser.RefreshToken); er == nil {
params.Result = "success"
params.OldToken = activeUser.RefreshToken
params.NewToken = wt.RefreshToken
activeUser.RefreshToken = wt.RefreshToken
activeUser.WebToken = *wt
activeUser.PanClient().UpdateToken(*wt)
logger.Verboseln("get new access token success")
// plugin callback
if er1 := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(activeUser), params); er1 != nil {
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er1.Error())
}
// create new signature
_, e := activeUser.PanClient().CreateSession(nil)
if e != nil {
logger.Verboseln("call CreateSession error in RefreshTokenInNeed: " + e.Error())
}
return true
} else {
// token refresh error
// if token has expired, callback plugin api for notify
if now.Unix() >= expiredTime.Unix() {
params.Result = "fail"
params.Message = er.Error()
params.OldToken = activeUser.RefreshToken
if er1 := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(activeUser), params); er1 != nil {
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er1.Error())
}
}
}
}
}
@@ -137,27 +192,125 @@ func RefreshTokenInNeed(activeUser *config.PanUser) bool {
return false
}
// ReloadRefreshTokenInNeed 从配置文件加载最新token
func ReloadRefreshTokenInNeed(activeUser *config.PanUser) bool {
if activeUser == nil {
func isIncludeFile(pattern string, fileName string) bool {
b, er := filepath.Match(pattern, fileName)
if er != nil {
return false
}
return b
}
// refresh expired token
if activeUser.PanClient() != nil {
if len(activeUser.WebToken.RefreshToken) > 0 {
cz := time.FixedZone("CST", 8*3600) // 东8区
expiredTime, _ := time.ParseInLocation("2006-01-02 15:04:05", activeUser.WebToken.ExpireTime, cz)
now := time.Now()
if (expiredTime.Unix() - now.Unix()) <= (10 * 60) { // 10min
// reload refresh token from config file
u := config.Config.ActiveUser()
activeUser.WebToken = u.WebToken
activeUser.PanClient().UpdateToken(u.WebToken)
logger.Verboseln("reload access token from config file")
return true
}
// isMatchWildcardPattern 是否是统配符字符串
func isMatchWildcardPattern(name string) bool {
return strings.ContainsAny(name, "*") || strings.ContainsAny(name, "?") || strings.ContainsAny(name, "[")
}
// DoLoginHelper 登录助手,使用token进行登录
func DoLoginHelper(refreshToken string) (refreshTokenStr string, webToken aliyunpan.WebLoginToken, error error) {
line := cmdliner.NewLiner()
defer line.Close()
if refreshToken == "" {
refreshToken, error = line.State.Prompt("请输入RefreshToken, 回车键提交 > ")
if error != nil {
return
}
}
return false
// app login
atoken, apperr := aliyunpan.GetAccessTokenFromRefreshToken(refreshToken)
if apperr != nil {
if apperr.Code == apierror.ApiCodeTokenExpiredCode || apperr.Code == apierror.ApiCodeRefreshTokenExpiredCode {
fmt.Println("Token过期,需要重新登录")
} else {
fmt.Println("Token登录失败:", apperr)
}
return "", webToken, fmt.Errorf("登录失败")
}
refreshTokenStr = refreshToken
return refreshTokenStr, *atoken, nil
}
// TryLogin 尝试登录,基础应用支持的各类登录方式进行尝试成功登录
func TryLogin() *config.PanUser {
// 获取当前插件
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
plugin, _ := pluginManger.GetPlugin()
params := &plugins.UserTokenRefreshFinishParams{
Result: "success",
Message: "",
OldToken: "",
NewToken: "",
UpdatedAt: utils.NowTimeStr(),
}
// can do automatically login?
for _, u := range config.Config.UserList {
if u.UserId == config.Config.ActiveUID {
// login
_, webToken, err := DoLoginHelper(u.RefreshToken)
if err != nil {
logger.Verboseln("automatically login use saved refresh token error ", err)
if u.TokenId != "" {
logger.Verboseln("try to login use tokenId")
h := panlogin.NewLoginHelper(config.DefaultTokenServiceWebHost)
r, e := h.GetRefreshToken(u.TokenId)
if e != nil {
logger.Verboseln("try to login use tokenId error", e)
// login fail plugin callback
params.Result = "fail"
params.OldToken = u.RefreshToken
params.NewToken = ""
if er := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(u), params); er != nil {
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er.Error())
}
break
}
refreshToken, e := h.ParseSecureRefreshToken("", r.SecureRefreshToken)
if e != nil {
logger.Verboseln("try to parse refresh token error", e)
break
}
_, webToken, err = DoLoginHelper(refreshToken)
if err != nil {
logger.Verboseln("try to use refresh token from tokenId error", e)
break
}
fmt.Println("Token重新自动登录成功")
// save new refresh token
u.RefreshToken = refreshToken
} else {
// login fail plugin callback
params.Result = "fail"
params.OldToken = u.RefreshToken
params.NewToken = ""
if er := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(u), params); er != nil {
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er.Error())
}
}
break
}
// plugin param
params.Result = "success"
params.OldToken = u.RefreshToken
params.NewToken = webToken.RefreshToken
// success login, save new token and access token
u.RefreshToken = webToken.RefreshToken
u.WebToken = webToken
// save
SaveConfigFunc(nil)
// reload
ReloadConfigFunc(nil)
// do plugin callback
if er := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(u), params); er != nil {
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er.Error())
}
return config.Config.ActiveUser()
}
}
return nil
}
+14 -6
View File
@@ -21,6 +21,7 @@ import (
"github.com/tickstep/library-go/logger"
"github.com/urfave/cli"
"strings"
"sync/atomic"
"time"
)
@@ -30,8 +31,8 @@ func CmdWebdav() cli.Command {
Usage: "在线网盘服务",
Description: "webdav在线网盘服务",
Category: "阿里云盘",
Before: cmder.ReloadConfigFunc,
After: cmder.SaveConfigFunc,
Before: ReloadConfigFunc,
After: SaveConfigFunc,
Action: func(c *cli.Context) error {
fmt.Print(`
本文命令可以让阿里云盘变身为webdav协议的文件服务器。这样你可以把阿里云盘挂载为Windows、Linux、Mac系统的磁盘,可以通过NAS系统做文件管理或文件同步等等。
@@ -72,16 +73,23 @@ aliyunpan webdav start -h
return nil
}
activeUser := GetActiveUser()
go func() {
for {
// pan token expired checker
continueFlag := int32(0)
atomic.StoreInt32(&continueFlag, 0)
defer func() {
atomic.StoreInt32(&continueFlag, 1)
}()
go func(flag *int32) {
for atomic.LoadInt32(flag) == 0 {
// token刷新
time.Sleep(time.Duration(1) * time.Minute)
//time.Sleep(time.Duration(5) * time.Second)
if ReloadRefreshTokenInNeed(activeUser) {
if RefreshTokenInNeed(activeUser, config.Config.DeviceName) {
logger.Verboseln("reload new access token for webdav")
}
}
}()
}(&continueFlag)
webdavServ := &webdav.WebdavConfig{
PanDriveId: "",
+24 -1
View File
@@ -58,6 +58,9 @@ const (
// DefaultVideoFileExtensions 默认的视频文件后缀
DefaultVideoFileExtensions = "mp4,flv,mkv,mov,rm,rmvb,wmv,wma,mv,asf,asx,mpg,mpeg,mpe,3gp,m4v,avi,vob"
// DefaultDeviceName 默认客户端名称
DefaultDeviceName = "Chrome浏览器"
)
var (
@@ -101,6 +104,10 @@ type PanConfig struct {
UpdateCheckInfo UpdateCheckInfo `json:"updateCheckInfo"`
VideoFileExtensions string `json:"videoFileExtensions"`
FileRecordConfig string `json:"fileRecordConfig"` // 上传、下载、同步文件的记录,包括失败和成功的
DeviceId string `json:"deviceId"` // 客户端ID,用于标识登录客户端,阿里单个账号最多允许10个客户端同时登录
DeviceName string `json:"deviceName"` // 客户端名称,默认为:Chrome浏览器
configFilePath string
configFile *os.File
@@ -250,6 +257,9 @@ func (c *PanConfig) loadConfigFromFile() (err error) {
if err != nil {
return ErrConfigContentsParseError
}
if c.DeviceName == "" {
c.DeviceName = DefaultDeviceName
}
return nil
}
@@ -272,6 +282,9 @@ func (c *PanConfig) initDefaultConfig() {
}
c.ConfigVer = ConfigVersion
c.VideoFileExtensions = DefaultVideoFileExtensions
c.DeviceId = RandomDeviceId() // 生成默认客户端ID
c.DeviceName = DefaultDeviceName
c.FileRecordConfig = "1" // 默认开启
}
// GetConfigDir 获取配置路径
@@ -323,6 +336,11 @@ func GetPluginDir() string {
return strings.TrimSuffix(GetConfigDir(), "/") + "/plugin"
}
// GetPluginKvFile 获取插件KV键值对存储文件
func GetPluginKvFile() string {
return GetPluginDir() + "/kv.bolt"
}
// GetSyncDriveDir 获取同步备份的文件夹路径
func GetSyncDriveDir() string {
return strings.TrimSuffix(GetConfigDir(), "/") + "/sync_drive"
@@ -344,6 +362,11 @@ func GetLogFilePath() string {
return dirPath + "/" + "aliyunpan_verbose.log"
}
// GetLockerDir 获取文件锁路径
func GetLockerDir() string {
return strings.TrimSuffix(GetConfigDir(), "/")
}
func (c *PanConfig) ActiveUser() *PanUser {
if c.activeUser == nil {
if c.UserList == nil {
@@ -356,7 +379,7 @@ func (c *PanConfig) ActiveUser() *PanUser {
if u.UserId == c.ActiveUID {
if u.PanClient() == nil {
// restore client
user, err := SetupUserByCookie(&u.WebToken)
user, err := SetupUserByCookie(&u.WebToken, c.DeviceId, c.DeviceName)
if err != nil {
logger.Verboseln("setup user error")
return nil
+25 -2
View File
@@ -66,8 +66,29 @@ func (c *PanConfig) SetMaxUploadRateByStr(sizeStr string) error {
return nil
}
// SetFileRecorderConfig 设置文件记录器
func (c *PanConfig) SetFileRecorderConfig(config string) error {
if config == "1" || config == "2" {
c.FileRecordConfig = config
}
return nil
}
// SetDeviceId 设置客户端ID
func (c *PanConfig) SetDeviceId(deviceId string) error {
if deviceId == "" {
return nil
}
c.DeviceId = deviceId
return nil
}
// PrintTable 输出表格
func (c *PanConfig) PrintTable() {
fileRecorderLabel := "禁用"
if c.FileRecordConfig == "1" {
fileRecorderLabel = "开启"
}
tb := cmdtable.NewTable(os.Stdout)
tb.SetHeader([]string{"名称", "值", "建议值", "描述"})
tb.SetHeaderAlignment(tablewriter.ALIGN_LEFT)
@@ -80,8 +101,10 @@ func (c *PanConfig) PrintTable() {
[]string{"max_upload_rate", showMaxRate(c.MaxUploadRate), "", "限制单个文件最大上传速度, 0代表不限制"},
[]string{"transfer_url_type", strconv.Itoa(c.TransferUrlType), "1-默认,2-阿里云ECS", "上传下载URL类别。除非在阿里云ECS(暂只支持经典网络)服务器中使用,不然请设置1"},
[]string{"savedir", c.SaveDir, "", "下载文件的储存目录"},
[]string{"proxy", c.Proxy, "", "设置代理, 支持 http/socks5 代理,例如:http://127.0.0.1:8888"},
[]string{"local_addrs", c.LocalAddrs, "", "设置本地网卡地址, 多个地址用逗号隔开,例如:127.0.0.1,192.168.100.126"},
[]string{"proxy", c.Proxy, "", "设置代理, 支持 http/socks5 代理,例如: http://127.0.0.1:8888 或者 socks5://127.0.0.1:8889"},
[]string{"local_addrs", c.LocalAddrs, "", "设置本地网卡地址, 多个地址用逗号隔开,例如: 127.0.0.1,192.168.100.126"},
[]string{"file_record_config", fileRecorderLabel, "1-开启,2-禁用", "设置是否开启上传、下载、同步文件的结果记录,开启后会把结果记录到CSV文件方便后期查看"},
[]string{"device_id", c.DeviceId, "", "客户端ID,用于标识登录客户端,阿里单个账号最多允许10个客户端同时在线。修改后需要重启应用生效"},
})
tb.Render()
}
+24 -2
View File
@@ -72,7 +72,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 +80,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,
@@ -126,6 +136,18 @@ doLoginAct:
// 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
}
+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())
}
+2 -1
View File
@@ -41,7 +41,8 @@ type Config struct {
InstanceStatePath string // 断点续传信息路径
TryHTTP bool // 是否尝试使用 http 连接
ShowProgress bool // 是否展示下载进度条
UseInternalUrl bool // 是否使用内置链接
UseInternalUrl bool // 是否使用内置链接
ExcludeNames []string // 排除的文件名,包括文件夹和文件。即这些文件/文件夹不进行下载,支持正则表达式
}
//NewConfig 返回默认配置
+14 -1
View File
@@ -28,6 +28,7 @@ import (
"github.com/tickstep/library-go/requester/rio/speeds"
"io"
"net/http"
"strings"
"sync"
"time"
)
@@ -382,13 +383,25 @@ 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)
cmdutil.Trigger(der.onCancelEvent)
return apierr
}
if durl == nil || durl.Url == "" || durl.Url == aliyunpan.IllegalDownloadUrl {
if durl == nil || durl.Url == "" || strings.HasPrefix(durl.Url, aliyunpan.IllegalDownloadUrlPrefix) {
logger.Verbosef("无法获取有效的下载链接: %+v\n", durl)
cmdutil.Trigger(der.onCancelEvent)
der.removeInstanceState() // 移除断点续传文件
+4
View File
@@ -16,6 +16,7 @@ package downloader
import (
"io"
"os"
"runtime"
)
type (
@@ -32,6 +33,9 @@ type (
// NewDownloaderWriterByFilename 创建下载器数据输出接口, 类似于os.OpenFile
func NewDownloaderWriterByFilename(name string, flag int, perm os.FileMode) (writer Writer, file *os.File, err error) {
if runtime.GOOS == "windows" {
// TODO: name进行特殊字符转换
}
file, err = os.OpenFile(name, flag, perm)
if err != nil {
return
+10 -1
View File
@@ -13,12 +13,21 @@
// limitations under the License.
package uploader
import "fmt"
var (
UploadUrlExpired = fmt.Errorf("UrlExpired")
UploadPartNotSeq = fmt.Errorf("PartNotSequential")
UploadTerminate = fmt.Errorf("UploadErrorTerminate")
UploadPartAlreadyExist = fmt.Errorf("PartAlreadyExist")
)
type (
// MultiError 多线程上传的错误
MultiError struct {
Err error
// IsRetry 是否重试,
Terminated bool
Terminated bool
NeedStartOver bool // 是否从头开始上传
}
)
+3 -2
View File
@@ -18,6 +18,7 @@ import (
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/library-go/converter"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/requester"
"github.com/tickstep/library-go/requester/rio"
"github.com/tickstep/library-go/requester/rio/speeds"
@@ -136,13 +137,13 @@ func (muer *MultiUploader) Execute() error {
// 分配任务
if muer.instanceState != nil {
muer.workers = muer.getWorkerListByInstanceState(muer.instanceState)
uploaderVerbose.Infof("upload task CREATED from instance state\n")
logger.Verboseln("upload task CREATED from instance state\n")
} else {
muer.workers = muer.getWorkerListByInstanceState(&InstanceState{
BlockList: SplitBlock(muer.file.Len(), muer.config.BlockSize),
})
uploaderVerbose.Infof("upload task CREATED: block size: %d, num: %d\n", muer.config.BlockSize, len(muer.workers))
logger.Verboseln("upload task CREATED: block size: %d, num: %d\n", muer.config.BlockSize, len(muer.workers))
}
// 开始上传
+76 -69
View File
@@ -17,6 +17,7 @@ import (
"context"
"github.com/oleiade/lane"
"github.com/tickstep/aliyunpan/internal/waitgroup"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/requester"
"os"
"strconv"
@@ -66,77 +67,83 @@ func (muer *MultiUploader) upload() (uperr error) {
uploadClient.SetKeepAlive(true)
for {
// 阿里云盘只支持分片按顺序上传,这里正常应该是parallel = 1
// 阿里云盘只支持分片按顺序上传,这里必须是parallel = 1
wg := waitgroup.NewWaitGroup(muer.config.Parallel)
for {
e := uploadDeque.Shift()
if e == nil { // 任务为空
wg.AddDelta()
uperr = nil
e := uploadDeque.Shift()
if e == nil { // 任务为空
break
}
wer := e.(*worker)
go func() { // 异步上传
defer wg.Done()
var (
ctx, cancel = context.WithCancel(context.Background())
doneChan = make(chan struct{})
uploadDone bool
terr error
)
go func() {
if !wer.uploadDone {
logger.Verboseln("begin to upload part: " + strconv.Itoa(wer.id))
uploadDone, terr = muer.multiUpload.UploadFile(ctx, int(wer.id), wer.partOffset, wer.splitUnit.Range().End, wer.splitUnit, uploadClient)
} else {
uploadDone = true
}
close(doneChan)
}()
select { // 监听上传进程,循环阻塞
case <-muer.canceled:
cancel()
return
case <-doneChan:
// continue
logger.Verboseln("multiUpload worker upload file done")
}
cancel()
if terr != nil {
logger.Verboseln("upload file part err: %+v", terr)
if me, ok := terr.(*MultiError); ok {
if me.Terminated { // 终止
muer.closeCanceledOnce.Do(func() { // 只关闭一次
close(muer.canceled)
})
uperr = me.Err
return
} else if me.NeedStartOver {
logger.Verboseln("upload start over: %d\n", wer.id)
// 从头开始上传
muer.closeCanceledOnce.Do(func() { // 只关闭一次
close(muer.canceled)
})
uperr = me.Err
return
}
}
logger.Verboseln("upload err: %s, id: %d\n", terr, wer.id)
wer.splitUnit.Seek(0, os.SEEK_SET)
uploadDeque.Prepend(wer) // 放回上传队列首位
return
}
wer.uploadDone = uploadDone
// 通知更新
if muer.updateInstanceStateChan != nil && len(muer.updateInstanceStateChan) < cap(muer.updateInstanceStateChan) {
muer.updateInstanceStateChan <- struct{}{}
}
}()
wg.Wait()
if uperr != nil {
if uperr == UploadPartNotSeq {
// 分片出现乱序,需要重新上传,取消本次所有剩余的分片的上传
break
}
wer := e.(*worker)
wg.AddDelta()
go func() {
defer wg.Done()
var (
ctx, cancel = context.WithCancel(context.Background())
doneChan = make(chan struct{})
uploadDone bool
terr error
)
go func() {
if !wer.uploadDone {
uploaderVerbose.Info("begin to upload part: " + strconv.Itoa(wer.id))
uploadDone, terr = muer.multiUpload.UploadFile(ctx, int(wer.id), wer.partOffset, wer.splitUnit.Range().End, wer.splitUnit, uploadClient)
} else {
uploadDone = true
}
close(doneChan)
}()
select {
case <-muer.canceled:
cancel()
return
case <-doneChan:
// continue
uploaderVerbose.Info("multiUpload worker upload file done")
}
cancel()
if terr != nil {
if me, ok := terr.(*MultiError); ok {
if me.Terminated { // 终止
muer.closeCanceledOnce.Do(func() { // 只关闭一次
close(muer.canceled)
})
uperr = me.Err
return
} else if me.NeedStartOver {
uploaderVerbose.Warnf("upload start over: %d\n", wer.id)
// 从头开始上传
muer.closeCanceledOnce.Do(func() { // 只关闭一次
close(muer.canceled)
})
uperr = me.Err
return
}
}
uploaderVerbose.Warnf("upload err: %s, id: %d\n", terr, wer.id)
wer.splitUnit.Seek(0, os.SEEK_SET)
uploadDeque.Append(wer)
return
}
wer.uploadDone = uploadDone
// 通知更新
if muer.updateInstanceStateChan != nil && len(muer.updateInstanceStateChan) < cap(muer.updateInstanceStateChan) {
muer.updateInstanceStateChan <- struct{}{}
}
}()
}
wg.Wait()
// 没有任务了
if uploadDeque.Size() == 0 {
break
@@ -164,11 +171,11 @@ func (muer *MultiUploader) upload() (uperr error) {
if allSuccess {
e := muer.multiUpload.CommitFile()
if e != nil {
uploaderVerbose.Warn("upload file commit failed: " + e.Error())
logger.Verboseln("upload file commit failed: " + e.Error())
return e
}
} else {
uploaderVerbose.Warn("upload file not all success: " + muer.UploadOpEntity.FileId)
logger.Verboseln("upload file not all success: " + muer.UploadOpEntity.FileId)
}
return
-6
View File
@@ -14,10 +14,8 @@
package uploader
import (
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/library-go/converter"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/requester"
"github.com/tickstep/library-go/requester/rio"
"net/http"
@@ -51,10 +49,6 @@ type (
}
)
var (
uploaderVerbose = logger.New("UPLOADER", config.EnvVerbose)
)
// NewUploader 返回 uploader 对象, url: 上传地址, readerlen64: 实现 rio.ReaderLen64 接口的对象, 例如文件
func NewUploader(url string, readerlen64 rio.ReaderLen64) (uploader *Uploader) {
uploader = &Uploader{
@@ -23,8 +23,10 @@ import (
"github.com/tickstep/aliyunpan/internal/file/downloader"
"github.com/tickstep/aliyunpan/internal/functions"
"github.com/tickstep/aliyunpan/internal/localfile"
"github.com/tickstep/aliyunpan/internal/log"
"github.com/tickstep/aliyunpan/internal/plugins"
"github.com/tickstep/aliyunpan/internal/taskframework"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/aliyunpan/library/requester/transfer"
"github.com/tickstep/library-go/converter"
"github.com/tickstep/library-go/logger"
@@ -66,6 +68,9 @@ type (
DriveId string
fileInfo *aliyunpan.FileEntity // 文件或目录详情
// 下载文件记录器
FileRecorder *log.FileRecorder
}
)
@@ -204,9 +209,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,
@@ -284,11 +291,13 @@ func (dtu *DownloadTaskUnit) panHTTPClient() (client *requester.HTTPClient) {
return client
}
// handleError 下载错误处理器
func (dtu *DownloadTaskUnit) handleError(result *taskframework.TaskUnitRunResult) {
switch value := result.Err.(type) {
case *apierror.ApiError:
switch value.ErrCode() {
case apierror.ApiCodeFileNotFoundCode:
case apierror.ApiCodeForbiddenFileInTheRecycleBin:
result.NeedRetry = false
break
default:
@@ -365,6 +374,18 @@ func (dtu *DownloadTaskUnit) OnRetry(lastRunResult *taskframework.TaskUnitRunRes
func (dtu *DownloadTaskUnit) OnSuccess(lastRunResult *taskframework.TaskUnitRunResult) {
// 执行插件
dtu.pluginCallback("success")
// 下载文件数据记录
if config.Config.FileRecordConfig == "1" {
if dtu.fileInfo.IsFile() {
dtu.FileRecorder.Append(&log.FileRecordItem{
Status: "成功",
TimeStr: utils.NowTimeStr(),
FileSize: dtu.fileInfo.FileSize,
FilePath: dtu.fileInfo.Path,
})
}
}
}
func (dtu *DownloadTaskUnit) OnFailed(lastRunResult *taskframework.TaskUnitRunResult) {
@@ -535,6 +556,13 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
// 创建对应的任务进行下载
for k := range fileList {
fileList[k].Path = path.Join(dtu.FilePanPath, fileList[k].FileName)
// 是否排除下载
if utils.IsExcludeFile(fileList[k].Path, &dtu.Cfg.ExcludeNames) {
fmt.Printf("排除文件: %s\n", fileList[k].Path)
continue
}
if fileList[k].IsFolder() {
logger.Verbosef("[%s] create sub folder download task: %s\n",
dtu.taskInfo.Id(), fileList[k].Path)
+26 -20
View File
@@ -16,7 +16,6 @@ package panupload
import (
"context"
"encoding/xml"
"fmt"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/requester"
"io"
@@ -45,13 +44,6 @@ type (
}
)
var (
uploadUrlExpired = fmt.Errorf("UrlExpired")
uploadPartNotSeq = fmt.Errorf("PartNotSequential")
uploadTerminate = fmt.Errorf("UploadErrorTerminate")
uploadPartAlreadyExist = fmt.Errorf("PartAlreadyExist")
)
func (e EmptyReaderLen64) Read(p []byte) (n int, err error) {
return 0, io.EOF
}
@@ -104,8 +96,9 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
}
newUploadInfo, err := pu.panClient.GetUploadUrl(refreshUploadParam)
if err != nil {
logger.Verboseln(err)
return false, &uploader.MultiError{
Err: uploadUrlExpired,
Err: uploader.UploadUrlExpired,
Terminated: false,
}
}
@@ -141,24 +134,24 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
if err := xml.Unmarshal(buf, errResp); err == nil {
if errResp.Code != "" {
if "PartNotSequential" == errResp.Code || "NoSuchUpload" == errResp.Code {
respError = uploadPartNotSeq
respError = uploader.UploadPartNotSeq
respErr = &uploader.MultiError{
Err: uploadPartNotSeq,
Err: uploader.UploadPartNotSeq,
Terminated: false,
NeedStartOver: true,
}
return resp, respError
} else if "AccessDenied" == errResp.Code && "Request has expired." == errResp.Message {
respError = uploadUrlExpired
respError = uploader.UploadUrlExpired
respErr = &uploader.MultiError{
Err: uploadUrlExpired,
Err: uploader.UploadUrlExpired,
Terminated: false,
}
return resp, respError
} else if "PartAlreadyExist" == errResp.Code {
respError = uploadPartAlreadyExist
respError = uploader.UploadPartAlreadyExist
respErr = &uploader.MultiError{
Err: uploadPartAlreadyExist,
Err: uploader.UploadPartAlreadyExist,
Terminated: false,
}
return resp, respError
@@ -173,13 +166,13 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
// 不可恢复的错误
switch resp.StatusCode {
case 400, 401, 403, 413, 600:
respError = uploadTerminate
respError = uploader.UploadTerminate
respErr = &uploader.MultiError{
Terminated: true,
}
}
} else {
respError = uploadTerminate
respError = uploader.UploadTerminate
respErr = &uploader.MultiError{
Terminated: true,
}
@@ -195,7 +188,7 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
apiError := pu.panClient.UploadFileData(uploadUrl, uploadFunc)
if respErr != nil {
if respErr.Err == uploadUrlExpired {
if respErr.Err == uploader.UploadUrlExpired {
// URL过期,获取新的URL
guur, er := pu.panClient.GetUploadUrl(&aliyunpan.GetUploadUrlParam{
DriveId: pu.driveId,
@@ -216,11 +209,11 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
uploadUrl = pu.uploadOpEntity.PartInfoList[partseq].InternalUploadURL
}
apiError = pu.panClient.UploadFileData(uploadUrl, uploadFunc)
} else if respErr.Err == uploadPartAlreadyExist {
} else if respErr.Err == uploader.UploadPartAlreadyExist {
// already upload
// success
return true, nil
} else if respErr.Err == uploadPartNotSeq {
} else if respErr.Err == uploader.UploadPartNotSeq {
// 上传分片乱序了,需要重新从0分片开始上传
// 先直接返回,后续再优化
return false, respErr
@@ -245,6 +238,19 @@ func (pu *PanUpload) CommitFile() (cerr error) {
FileId: pu.uploadOpEntity.FileId,
UploadId: pu.uploadOpEntity.UploadId,
})
if er != nil && er.Code == apierror.ApiCodeDeviceSessionSignatureInvalid {
_, e := pu.panClient.CreateSession(nil)
if e == nil {
// retry
_, er = pu.panClient.CompleteUploadFile(&aliyunpan.CompleteUploadFileParam{
DriveId: pu.driveId,
FileId: pu.uploadOpEntity.FileId,
UploadId: pu.uploadOpEntity.UploadId,
})
} else {
logger.Verboseln("CreateSession failed")
}
}
if er != nil {
return er
}
@@ -15,6 +15,7 @@ package panupload
import (
"fmt"
"github.com/tickstep/aliyunpan/internal/log"
"github.com/tickstep/aliyunpan/internal/plugins"
"github.com/tickstep/library-go/logger"
"os"
@@ -71,6 +72,9 @@ type (
// 全局速度统计
GlobalSpeedsStat *speeds.Speeds
// 上传文件记录器
FileRecorder *log.FileRecorder
}
)
@@ -118,11 +122,11 @@ func (utu *UploadTaskUnit) prepareFile() {
return
}
if utu.LocalFileChecksum.Length > MaxRapidUploadSize {
fmt.Printf("[%s] 文件超过20GB, 无法使用秒传功能, 跳过秒传...\n", utu.taskInfo.Id())
utu.Step = StepUploadUpload
return
}
//if utu.LocalFileChecksum.Length > MaxRapidUploadSize {
// fmt.Printf("[%s] 文件超过20GB, 无法使用秒传功能, 跳过秒传...\n", utu.taskInfo.Id())
// utu.Step = StepUploadUpload
// return
//}
// 下一步: 秒传
utu.Step = StepUploadRapidUpload
}
@@ -175,9 +179,11 @@ func (utu *UploadTaskUnit) upload() (result *taskframework.TaskUnitRunResult) {
}
if utu.ShowProgress {
fmt.Printf("\r[%s] ↑ %s/%s %s/s(%s/s) in %s ............", utu.taskInfo.Id(),
uploadedPercentage := fmt.Sprintf("%.2f%%", float64(status.Uploaded())/float64(status.TotalSize())*100)
fmt.Printf("\r[%s] ↑ %s/%s(%s) %s/s(%s/s) in %s ............", utu.taskInfo.Id(),
converter.ConvertFileSize(status.Uploaded(), 2),
converter.ConvertFileSize(status.TotalSize(), 2),
uploadedPercentage,
converter.ConvertFileSize(status.SpeedsPerSecond(), 2),
converter.ConvertFileSize(utu.GlobalSpeedsStat.GetSpeeds(), 2),
status.TimeElapsed(),
@@ -239,6 +245,16 @@ func (utu *UploadTaskUnit) OnRetry(lastRunResult *taskframework.TaskUnitRunResul
func (utu *UploadTaskUnit) OnSuccess(lastRunResult *taskframework.TaskUnitRunResult) {
// 执行插件
utu.pluginCallback("success")
// 上传文件数据记录
if config.Config.FileRecordConfig == "1" {
utu.FileRecorder.Append(&log.FileRecordItem{
Status: "成功",
TimeStr: utils.NowTimeStr(),
FileSize: utu.LocalFileChecksum.LocalFileMeta.Length,
FilePath: utu.LocalFileChecksum.Path.LogicPath,
})
}
}
func (utu *UploadTaskUnit) OnFailed(lastRunResult *taskframework.TaskUnitRunResult) {
@@ -320,6 +336,7 @@ func (utu *UploadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
var proofCode = ""
var localFileInfo os.FileInfo
var localFile *os.File
var newBlockSize int64
switch utu.Step {
case StepUploadPrepareUpload:
@@ -335,24 +352,42 @@ StepUploadPrepareUpload:
// 创建云盘文件夹
saveFilePath = path.Dir(utu.SavePath)
if saveFilePath != "/" {
utu.FolderCreateMutex.Lock()
fmt.Printf("[%s] %s 正在检测和创建云盘文件夹: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), saveFilePath)
fe, apierr1 := utu.PanClient.FileInfoByPath(utu.DriveId, saveFilePath)
time.Sleep(1 * time.Second)
needToCreateFolder := false
if apierr1 != nil && apierr1.Code == apierror.ApiCodeFileNotFoundCode {
needToCreateFolder = true
} else {
if fe == nil {
needToCreateFolder = true
} else {
rs = &aliyunpan.MkdirResult{}
rs.FileId = fe.FileId
}
}
if needToCreateFolder {
logger.Verbosef("[%s] %s 创建云盘文件夹: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), saveFilePath)
// rs, apierr = utu.PanClient.MkdirRecursive(utu.DriveId, "", "", 0, strings.Split(path.Clean(saveFilePath), "/"))
// 可以直接创建的,不用循环创建
rs, apierr = utu.PanClient.Mkdir(utu.DriveId, "root", saveFilePath)
if apierr != nil && apierr.Code == apierror.ApiCodeDeviceSessionSignatureInvalid {
_, e := utu.PanClient.CreateSession(nil)
if e == nil {
// retry
rs, apierr = utu.PanClient.Mkdir(utu.DriveId, "root", saveFilePath)
} else {
logger.Verboseln("CreateSession failed")
}
}
if apierr != nil || rs.FileId == "" {
result.Err = apierr
result.ResultMessage = "创建云盘文件夹失败"
utu.FolderCreateMutex.Unlock()
return
}
logger.Verbosef("[%s] %s 创建云盘文件夹成功\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
} else {
rs = &aliyunpan.MkdirResult{}
rs.FileId = fe.FileId
}
utu.FolderCreateMutex.Unlock()
} else {
rs = &aliyunpan.MkdirResult{}
rs.FileId = ""
@@ -413,6 +448,13 @@ StepUploadPrepareUpload:
}
}
// 自动调整BlockSize大小
newBlockSize = utils.ResizeUploadBlockSize(utu.LocalFileChecksum.Length, utu.BlockSize)
if newBlockSize != utu.BlockSize {
logger.Verboseln("resize upload block size to: " + converter.ConvertFileSize(newBlockSize, 2))
utu.BlockSize = newBlockSize
}
// 创建上传任务
appCreateUploadFileParam = &aliyunpan.CreateFileUploadParam{
DriveId: utu.DriveId,
@@ -428,9 +470,23 @@ 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()
if apierr.Code == apierror.ApiCodeTooManyRequests || apierr.Code == apierror.ApiCodeBadGateway {
logger.Verboseln("create upload file error: " + result.ResultMessage)
// 重试
result.NeedRetry = true
}
return
}
@@ -451,7 +507,7 @@ stepUploadUpload:
// 正常上传流程
uploadResult := utu.upload()
if uploadResult != nil && uploadResult.Err != nil {
if uploadResult.Err == uploadPartNotSeq {
if uploadResult.Err == uploader.UploadPartNotSeq {
fmt.Printf("[%s] %s 文件分片上传顺序错误,开始重新上传文件\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
// 需要重新从0开始上传
uploadResult = nil
+1 -1
View File
@@ -55,7 +55,7 @@ func IsUrlExpired(urlStr string) bool {
}
expiredTimeSecStr := u.Query().Get("x-oss-expires")
expiredTimeSec, _ := strconv.ParseInt(expiredTimeSecStr, 10, 64)
if (time.Now().Unix() - 10) >= expiredTimeSec {
if (expiredTimeSec - time.Now().Unix()) <= 300 { // 小于5分钟
// expired
return true
}
+1 -3
View File
@@ -14,7 +14,6 @@
package functions
import (
"github.com/tickstep/library-go/expires"
"sync/atomic"
"time"
)
@@ -36,9 +35,8 @@ func (s *Statistic) TotalSize() int64 {
func (s *Statistic) StartTimer() {
s.startTime = time.Now()
expires.StripMono(&s.startTime)
}
func (s *Statistic) Elapsed() time.Duration {
return time.Now().Sub(s.startTime)
}
}
+76
View File
@@ -0,0 +1,76 @@
package log
import (
"encoding/csv"
"fmt"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/library-go/converter"
"github.com/tickstep/library-go/logger"
"os"
"path/filepath"
"sync"
)
type (
FileRecordItem struct {
Status string `json:"status"`
TimeStr string `json:"timeStr"`
FileSize int64 `json:"fileSize"`
FilePath string `json:"filePath"`
}
FileRecorder struct {
Path string `json:"path"`
locker *sync.Mutex
}
)
// NewFileRecorder 创建文件记录器
func NewFileRecorder(filePath string) *FileRecorder {
return &FileRecorder{
Path: filePath,
locker: &sync.Mutex{},
}
}
// Append 增加数据记录
func (f *FileRecorder) Append(item *FileRecordItem) error {
f.locker.Lock()
defer f.locker.Unlock()
savePath := f.Path
folder := filepath.Dir(savePath)
if b, err := utils.PathExists(folder); err == nil && !b {
os.MkdirAll(folder, 0755)
}
var fp *os.File
var write *csv.Writer
if b, err := utils.PathExists(savePath); err == nil && b {
file, err1 := os.OpenFile(savePath, os.O_APPEND, 0755)
if err1 != nil {
logger.Verbosef("打开文件["+savePath+"]失败,%v", err1)
return err1
}
fp = file
write = csv.NewWriter(fp) //创建一个新的写入文件流
} else {
file, err1 := os.OpenFile(savePath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0755) // 创建文件句柄
if err1 != nil {
logger.Verbosef("创建文件["+savePath+"]失败,%v", err1)
return err1
}
fp = file
fp.WriteString("\xEF\xBB\xBF") // 写入UTF-8 BOM
write = csv.NewWriter(fp) //创建一个新的写入文件流
write.Write([]string{"状态", "时间", "文件大小", "文件路径"})
}
if fp == nil || write == nil {
return fmt.Errorf("open recorder file error")
}
defer fp.Close()
data := []string{item.Status, item.TimeStr, converter.ConvertFileSize(item.FileSize, 2), item.FilePath}
write.Write(data)
write.Flush()
return nil
}
+16
View File
@@ -0,0 +1,16 @@
package log
import (
"testing"
)
func TestCsvFile(t *testing.T) {
savePath := "D:\\smb\\feny\\goprojects\\dev\\logs\\file_upload_records.csv"
recorder := NewFileRecorder(savePath)
recorder.Append(&FileRecordItem{
Status: "成功",
TimeStr: "2022-12-19 16:46:36",
FileSize: 453450,
FilePath: "D:\\smb\\feny\\goprojects\\dev\\myfile.mp4",
})
}
+39 -8
View File
@@ -17,10 +17,12 @@ import (
"archive/zip"
"bytes"
"fmt"
jsoniter "github.com/json-iterator/go"
"github.com/tickstep/aliyunpan/cmder/cmdliner"
"github.com/tickstep/aliyunpan/cmder/cmdutil"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/aliyunpan/library/requester/transfer"
"github.com/tickstep/library-go/cachepool"
"github.com/tickstep/library-go/checkaccess"
"github.com/tickstep/library-go/converter"
@@ -28,7 +30,7 @@ import (
"github.com/tickstep/library-go/jsonhelper"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/requester"
"github.com/tickstep/aliyunpan/library/requester/transfer"
"io/ioutil"
"net/http"
"path/filepath"
"regexp"
@@ -50,14 +52,14 @@ type info struct {
}
type tsResp struct {
Code int `json:"code"`
Code int `json:"code"`
Data interface{} `json:"data"`
Msg string `json:"msg"`
Msg string `json:"msg"`
}
func getReleaseFromTicstep(client *requester.HTTPClient, showPrompt bool) *ReleaseInfo {
tsReleaseInfo := &ReleaseInfo{}
tsResp := &tsResp{Data: tsReleaseInfo}
tsRespObj := &tsResp{Data: tsReleaseInfo}
fullUrl := strings.Builder{}
ipAddr, err := getip.IPInfoFromTechainBaidu()
if err != nil {
@@ -75,15 +77,32 @@ func getReleaseFromTicstep(client *requester.HTTPClient, showPrompt bool) *Relea
}
return nil
}
err = jsonhelper.UnmarshalData(resp.Body, tsResp)
respBytes, _ := ioutil.ReadAll(resp.Body)
// 解析响应返回
tsRespEntity := &tsResp{}
if err = jsoniter.Unmarshal(respBytes, tsRespEntity); err == nil {
if tsRespEntity.Code != 0 {
if showPrompt {
fmt.Printf("错误: %s\n", tsRespEntity.Msg)
}
logger.Verboseln(string(respBytes))
return nil
}
}
// 解析可用版本返回
err = jsoniter.Unmarshal(respBytes, tsRespObj)
if err != nil {
if showPrompt {
fmt.Printf("json数据解析失败: %s\n", err)
}
return nil
}
if tsResp.Code == 0 {
return tsReleaseInfo
if tsRespObj.Code == 0 {
if tsReleaseInfo != nil && len(tsReleaseInfo.Assets) > 0 {
return tsReleaseInfo
}
}
return nil
}
@@ -278,7 +297,15 @@ func CheckUpdate(version string, yes bool) {
// 开始下载
buf := cachepool.RawMallocByteSlice(int(target.size))
resp, err := client.Req("GET", target.downloadURL, nil, nil)
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
logger.Verboseln("下载文件:" + req.URL.String())
req.Header.Del("Referer") // aliyundrive会检测盗链行为,所以删除Referer
return nil
}
header := map[string]string{}
header["Referer"] = ""
header["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36 Edg/106.0.1370.42"
resp, err := client.Req("GET", target.downloadURL, nil, header)
if err != nil {
fmt.Printf("下载更新文件发生错误: %s\n", err)
return
@@ -286,6 +313,10 @@ func CheckUpdate(version string, yes bool) {
total, _ := strconv.Atoi(resp.Header.Get("Content-Length"))
if total > 0 {
if int64(total) != target.size {
if es, e := ioutil.ReadAll(resp.Body); e == nil {
// 发生错误
logger.Verboseln(string(es))
}
fmt.Printf("下载更新文件发生错误: %s\n", err)
return
}
+8
View File
@@ -40,6 +40,14 @@ func (p *IdlePlugin) SyncScanPanFilePrepareCallback(context *Context, params *Sy
return nil, nil
}
func (p *IdlePlugin) SyncFileFinishCallback(context *Context, params *SyncFileFinishParams) error {
return nil
}
func (p *IdlePlugin) UserTokenRefreshFinishCallback(context *Context, params *UserTokenRefreshFinishParams) error {
return nil
}
func (p *IdlePlugin) Stop() error {
return nil
}
+51
View File
@@ -3,6 +3,7 @@ package plugins
import (
"fmt"
"github.com/dop251/goja"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/library-go/logger"
"strings"
)
@@ -55,6 +56,19 @@ func (js *JsPlugin) Start() error {
pluginObj.Set("LocalFS", localFS)
localFS.Set("deleteFile", DeleteLocalFile) // PluginUtil.LocalFS.deleteFile()
// PluginUtil.Email
emailObj := js.vm.NewObject()
pluginObj.Set("Email", emailObj)
emailObj.Set("sendTextMail", SendTextMail) // PluginUtil.Email.sendTextMail()
emailObj.Set("sendHtmlMail", SendHtmlMail) // PluginUtil.Email.sendHtmlMail()
// PluginUtil.KV
PersistenceFilePath = config.GetPluginKvFile()
kvObj := js.vm.NewObject()
pluginObj.Set("KV", kvObj)
kvObj.Set("getString", GetString) // PluginUtil.KV.getString()
kvObj.Set("putString", PutString) // PluginUtil.KV.putString()
return nil
}
@@ -190,6 +204,43 @@ func (js *JsPlugin) SyncScanPanFilePrepareCallback(context *Context, params *Syn
return r, nil
}
// SyncFileFinishCallback 同步备份-同步文件完成的回调函数
func (js *JsPlugin) SyncFileFinishCallback(context *Context, params *SyncFileFinishParams) error {
var fn func(*Context, *SyncFileFinishParams) error
if !js.isHandlerFuncExisted("syncFileFinishCallback") {
return nil
}
err := js.vm.ExportTo(js.vm.Get("syncFileFinishCallback"), &fn)
if err != nil {
logger.Verboseln("Js函数映射到 Go 函数失败!")
return nil
}
er := fn(context, params)
if er != nil {
logger.Verboseln(er)
return nil
}
return nil
}
func (js *JsPlugin) UserTokenRefreshFinishCallback(context *Context, params *UserTokenRefreshFinishParams) error {
var fn func(*Context, *UserTokenRefreshFinishParams) error
if !js.isHandlerFuncExisted("userTokenRefreshFinishCallback") {
return nil
}
err := js.vm.ExportTo(js.vm.Get("userTokenRefreshFinishCallback"), &fn)
if err != nil {
logger.Verboseln("Js函数映射到 Go 函数失败!")
return nil
}
er := fn(context, params)
if er != nil {
logger.Verboseln(er)
return nil
}
return nil
}
func (js *JsPlugin) Stop() error {
return nil
}
+28
View File
@@ -122,6 +122,28 @@ type (
SyncScanPanApproved string `json:"syncScanPanApproved"`
}
// SyncFileFinishParams 同步备份-文件同步结束-回调参数
SyncFileFinishParams struct {
Action string `json:"action"`
ActionResult string `json:"actionResult"`
DriveId string `json:"driveId"`
FileName string `json:"fileName"`
FilePath string `json:"filePath"`
FileSha1 string `json:"fileSha1"`
FileSize int64 `json:"fileSize"`
FileType string `json:"fileType"`
FileUpdatedAt string `json:"fileUpdatedAt"`
}
// UserTokenRefreshFinishParams 用户Token刷新完成后回调函数
UserTokenRefreshFinishParams struct {
Result string `json:"result"`
Message string `json:"message"`
OldToken string `json:"oldToken"`
NewToken string `json:"newToken"`
UpdatedAt string `json:"updatedAt"`
}
// Plugin 插件接口
Plugin interface {
// Start 启动
@@ -145,6 +167,12 @@ type (
// SyncScanPanFilePrepareCallback 同步备份-扫描本地文件的回调函数
SyncScanPanFilePrepareCallback(context *Context, params *SyncScanPanFilePrepareParams) (*SyncScanPanFilePrepareResult, error)
// SyncFileFinishCallback 同步备份-同步文件完成的回调函数
SyncFileFinishCallback(context *Context, params *SyncFileFinishParams) error
// UserTokenRefreshFinishCallback 用户Token刷新完成后回调函数
UserTokenRefreshFinishCallback(context *Context, params *UserTokenRefreshFinishParams) error
// Stop 停止
Stop() error
}
+126
View File
@@ -1,9 +1,30 @@
package plugins
import (
"crypto/tls"
"github.com/jordan-wright/email"
jsoniter "github.com/json-iterator/go"
"github.com/tickstep/bolt"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/requester"
"net/smtp"
"os"
"strings"
"sync"
"time"
)
type (
PersistenceItem struct {
Key string `json:"key"`
Value string `json:"value"`
Type string `json:"type"`
}
)
var (
locker = &sync.Mutex{}
PersistenceFilePath = ""
)
// HttpGet Http的get请求
@@ -40,3 +61,108 @@ func DeleteLocalFile(localFilePath string) bool {
}
return false
}
// SendTextMail 发送文本邮件
func SendTextMail(mailServer, userName, password, to, subject, body string) bool {
if e := sendEmail(mailServer, userName, password, to, subject, body, "text", true); e != nil {
logger.Verboseln("js SendTextMail error ", e)
return false
}
return true
}
// SendHtmlMail 发送HTML富文本邮件
func SendHtmlMail(mailServer, userName, password, to, subject, body string) bool {
if e := sendEmail(mailServer, userName, password, to, subject, body, "html", true); e != nil {
logger.Verboseln("js SendHtmlMail error ", e)
return false
}
return true
}
func sendEmail(mailServer, userName, password, to, subject, body, mailType string, useSsl bool) error {
mailServerHost := strings.Split(mailServer, ":")[0]
auth := smtp.PlainAuth("", userName, password, mailServerHost)
e := email.NewEmail()
e.From = userName
e.To = []string{to}
e.Subject = subject
if mailType == "html" {
e.HTML = []byte(body)
} else {
e.Text = []byte(body)
}
if useSsl {
return e.SendWithStartTLS(mailServer, auth, &tls.Config{ServerName: mailServerHost, InsecureSkipVerify: true})
} else {
return e.Send(mailServer, auth)
}
}
// GetString 获取值
func GetString(key string) string {
locker.Lock()
defer locker.Unlock()
db, err := bolt.Open(PersistenceFilePath, 0755, &bolt.Options{Timeout: 5 * time.Second})
if err != nil {
return ""
}
defer db.Close()
tx, err := db.Begin(false)
if err != nil {
return ""
}
bkt := tx.Bucket([]byte("/"))
if bkt == nil {
return ""
}
data := bkt.Get([]byte(key))
item := PersistenceItem{}
if e := jsoniter.Unmarshal(data, &item); e == nil {
return item.Value
}
return ""
}
// PutString 存储KV键值对
func PutString(key, value string) bool {
locker.Lock()
defer locker.Unlock()
db, err := bolt.Open(PersistenceFilePath, 0755, &bolt.Options{Timeout: 5 * time.Second})
if err != nil {
return false
}
defer db.Close()
// Start a writable transaction.
tx, err := db.Begin(true)
if err != nil {
return false
}
defer tx.Rollback()
rootBucket, er := tx.CreateBucketIfNotExists([]byte("/"))
if er != nil {
return false
}
data, ee := jsoniter.Marshal(&PersistenceItem{
Key: key,
Value: value,
Type: "string",
})
if ee != nil {
return false
}
if e := rootBucket.Put([]byte(key), data); e != nil {
return false
}
// Commit the transaction and check for error.
if err := tx.Commit(); err != nil {
return false
}
return true
}
+15
View File
@@ -8,3 +8,18 @@ import (
func TestDeleteLocalFile(t *testing.T) {
fmt.Println(DeleteLocalFile("/Volumes/Downloads/dev/upload/2"))
}
func TestSendEmail(t *testing.T) {
fmt.Println(sendEmail("smtp.qq.com:465", "111xxx@qq.com", "xxxxxx", "12545xxx@qq.com", "title", "hello", "text", true))
}
func TestPutString(t *testing.T) {
PersistenceFilePath = "/Volumes/Downloads/kv.bolt"
PutString("test1", "ok1234-new")
}
func TestGetString(t *testing.T) {
PersistenceFilePath = "/Volumes/Downloads/kv.bolt"
v := GetString("test1")
fmt.Println(v)
}
+73 -3
View File
@@ -5,12 +5,15 @@ import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/internal/file/downloader"
"github.com/tickstep/aliyunpan/internal/file/uploader"
"github.com/tickstep/aliyunpan/internal/functions/panupload"
"github.com/tickstep/aliyunpan/internal/localfile"
"github.com/tickstep/aliyunpan/internal/log"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/aliyunpan/library/requester/transfer"
"github.com/tickstep/library-go/converter"
"github.com/tickstep/library-go/logger"
"github.com/tickstep/library-go/requester"
"github.com/tickstep/library-go/requester/rio"
@@ -38,6 +41,9 @@ type (
localFolderCreateMutex *sync.Mutex
panFolderCreateMutex *sync.Mutex
// 文件记录器,存储同步文件记录
fileRecorder *log.FileRecorder
}
)
@@ -67,12 +73,15 @@ func (f *FileActionTask) DoAction(ctx context.Context) error {
} else {
// upload success, post operation
// save local file info into db
var actFile *aliyunpan.FileEntity
if f.syncItem.UploadEntity != nil && f.syncItem.UploadEntity.FileId != "" {
if file, er := f.panClient.FileInfoById(f.syncItem.DriveId, f.syncItem.UploadEntity.FileId); er == nil {
file.Path = f.syncItem.getPanFileFullPath()
fItem := NewPanFileItem(file)
fItem.ScanTimeAt = utils.NowTimeStr()
f.panFileDb.Add(fItem)
actFile = file
}
} else {
if file, er := f.panClient.FileInfoByPath(f.syncItem.DriveId, f.syncItem.getPanFileFullPath()); er == nil {
@@ -80,8 +89,20 @@ func (f *FileActionTask) DoAction(ctx context.Context) error {
fItem := NewPanFileItem(file)
fItem.ScanTimeAt = utils.NowTimeStr()
f.panFileDb.Add(fItem)
actFile = file
}
}
// recorder
if actFile != nil {
f.appendRecord(&log.FileRecordItem{
Status: "成功-上传",
TimeStr: utils.NowTimeStr(),
FileSize: actFile.FileSize,
FilePath: actFile.Path,
})
}
}
}
@@ -136,6 +157,14 @@ func (f *FileActionTask) DoAction(ctx context.Context) error {
ScanStatus: ScanStatusNormal,
})
}
// recorder
f.appendRecord(&log.FileRecordItem{
Status: "成功-下载",
TimeStr: utils.NowTimeStr(),
FileSize: f.syncItem.PanFile.FileSize,
FilePath: f.syncItem.PanFile.Path,
})
}
}
@@ -148,6 +177,14 @@ func (f *FileActionTask) DoAction(ctx context.Context) error {
// clear DB
f.localFileDb.Delete(f.syncItem.getLocalFileFullPath())
f.panFileDb.Delete(f.syncItem.getPanFileFullPath())
// recorder
f.appendRecord(&log.FileRecordItem{
Status: "成功-删除本地文件",
TimeStr: utils.NowTimeStr(),
FileSize: 0,
FilePath: f.syncItem.getLocalFileFullPath(),
})
}
}
@@ -160,6 +197,14 @@ func (f *FileActionTask) DoAction(ctx context.Context) error {
// clear DB
f.localFileDb.Delete(f.syncItem.getLocalFileFullPath())
f.panFileDb.Delete(f.syncItem.getPanFileFullPath())
// recorder
f.appendRecord(&log.FileRecordItem{
Status: "成功-删除云盘文件",
TimeStr: utils.NowTimeStr(),
FileSize: 0,
FilePath: f.syncItem.getPanFileFullPath(),
})
}
}
@@ -211,7 +256,7 @@ func (f *FileActionTask) downloadFile(ctx context.Context) error {
})
time.Sleep(time.Duration(200) * time.Millisecond)
if apierr != nil {
if apierr.Code == apierror.ApiCodeFileNotFoundCode {
if apierr.Code == apierror.ApiCodeFileNotFoundCode || apierr.Code == apierror.ApiCodeForbiddenFileInTheRecycleBin {
f.syncItem.Status = SyncFileStatusNotExisted
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
f.syncFileDb.Update(f.syncItem)
@@ -227,7 +272,7 @@ func (f *FileActionTask) downloadFile(ctx context.Context) error {
f.syncFileDb.Update(f.syncItem)
return fmt.Errorf("无法获取有效的下载链接")
}
if durl.Url == aliyunpan.IllegalDownloadUrl {
if strings.HasPrefix(durl.Url, aliyunpan.IllegalDownloadUrlPrefix) {
logger.Verbosef("无法获取有效的下载链接: %+v\n", durl)
f.syncItem.Status = SyncFileStatusIllegal
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
@@ -364,6 +409,9 @@ func (f *FileActionTask) uploadFile(ctx context.Context) error {
err := localFile.OpenPath()
if err != nil {
logger.Verbosef("文件不可读 %s, 错误信息: %s\n", localFile.Path, err)
f.syncItem.Status = SyncFileStatusFailed
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
f.syncFileDb.Update(f.syncItem)
return err
}
defer localFile.Close() // 关闭文件
@@ -447,6 +495,15 @@ func (f *FileActionTask) uploadFile(ctx context.Context) error {
localFileInfo, _ := localFileEntity.Stat()
proofCode = aliyunpan.CalcProofCode(f.panClient.GetAccessToken(), rio.NewFileReaderAtLen64(localFileEntity), localFileInfo.Size())
// 自动调整BlockSize大小
newBlockSize := utils.ResizeUploadBlockSize(localFile.Length, f.syncItem.UploadBlockSize)
if newBlockSize != f.syncItem.UploadBlockSize {
logger.Verboseln("resize upload block size to: " + converter.ConvertFileSize(newBlockSize, 2))
f.syncItem.UploadBlockSize = newBlockSize
// 存储状态
f.syncFileDb.Update(f.syncItem)
}
// 创建上传任务
appCreateUploadFileParam := &aliyunpan.CreateFileUploadParam{
DriveId: f.syncItem.DriveId,
@@ -637,7 +694,7 @@ func (f *FileActionTask) deletePanFile(ctx context.Context) error {
// 删除
var fileDeleteResult []*aliyunpan.FileBatchActionResult
var err *apierror.ApiError
var err *apierror.ApiError = nil
fileDeleteResult, err = f.panClient.FileDelete([]*aliyunpan.FileBatchActionParam{{DriveId: driveId, FileId: panFileId}})
time.Sleep(1 * time.Second)
if err != nil || len(fileDeleteResult) == 0 {
@@ -647,6 +704,9 @@ func (f *FileActionTask) deletePanFile(ctx context.Context) error {
}
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
f.syncFileDb.Update(f.syncItem)
if err == nil {
return nil
}
return err
}
@@ -700,3 +760,13 @@ func (f *FileActionTask) createPanFolder(ctx context.Context) error {
return apierr1
}
}
func (f *FileActionTask) appendRecord(item *log.FileRecordItem) error {
if item == nil {
return nil
}
if config.Config.FileRecordConfig == "1" {
return f.fileRecorder.Append(item)
}
return nil
}
@@ -5,7 +5,9 @@ import (
"fmt"
mapset "github.com/deckarep/golang-set"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/internal/localfile"
"github.com/tickstep/aliyunpan/internal/plugins"
"github.com/tickstep/aliyunpan/internal/waitgroup"
"github.com/tickstep/aliyunpan/library/collection"
"github.com/tickstep/library-go/logger"
@@ -36,6 +38,12 @@ type (
panFolderModifyCount int // 云盘文件扫描变更记录次数,作为后续文件对比进程的参考以节省CPU资源
syncActionModifyCount int // 文件对比进程检测的文件上传下载删除变更记录次数,作为后续文件上传下载处理进程的参考以节省CPU资源
resourceModifyMutex *sync.Mutex
panUser *config.PanUser
// 插件
plugin plugins.Plugin
pluginMutex *sync.Mutex
}
localFileSet struct {
@@ -62,6 +70,8 @@ func NewFileActionTaskManager(task *SyncTask) *FileActionTaskManager {
panFolderModifyCount: 1,
syncActionModifyCount: 1,
resourceModifyMutex: &sync.Mutex{},
panUser: task.panUser,
}
}
@@ -140,6 +150,14 @@ func (f *FileActionTaskManager) Start() error {
f.ctx, cancel = context.WithCancel(context.Background())
f.cancelFunc = cancel
if f.plugin == nil {
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
f.plugin, _ = pluginManger.GetPlugin()
}
if f.pluginMutex == nil {
f.pluginMutex = &sync.Mutex{}
}
go f.doLocalFileDiffRoutine(f.ctx)
go f.doPanFileDiffRoutine(f.ctx)
go f.fileActionTaskExecutor(f.ctx)
@@ -711,6 +729,7 @@ func (f *FileActionTaskManager) getFromSyncDb(act SyncFileAction) *FileActionTas
maxUploadRate: f.syncOption.MaxUploadRate,
localFolderCreateMutex: f.localCreateMutex,
panFolderCreateMutex: f.folderCreateMutex,
fileRecorder: f.syncOption.FileRecorder,
}
}
}
@@ -729,6 +748,7 @@ func (f *FileActionTaskManager) getFromSyncDb(act SyncFileAction) *FileActionTas
maxUploadRate: f.syncOption.MaxUploadRate,
localFolderCreateMutex: f.localCreateMutex,
panFolderCreateMutex: f.folderCreateMutex,
fileRecorder: f.syncOption.FileRecorder,
}
}
}
@@ -749,6 +769,7 @@ func (f *FileActionTaskManager) getFromSyncDb(act SyncFileAction) *FileActionTas
maxUploadRate: f.syncOption.MaxUploadRate,
localFolderCreateMutex: f.localCreateMutex,
panFolderCreateMutex: f.folderCreateMutex,
fileRecorder: f.syncOption.FileRecorder,
}
}
}
@@ -798,9 +819,11 @@ func (f *FileActionTaskManager) fileActionTaskExecutor(ctx context.Context) {
if e := uploadItem.DoAction(ctx); e == nil {
// success
f.fileInProcessQueue.Remove(uploadItem.syncItem)
f.doPluginCallback(uploadItem.syncItem, "success")
} else {
// retry?
f.fileInProcessQueue.Remove(uploadItem.syncItem)
f.doPluginCallback(uploadItem.syncItem, "fail")
}
uploadWaitGroup.Done()
}()
@@ -818,9 +841,11 @@ func (f *FileActionTaskManager) fileActionTaskExecutor(ctx context.Context) {
if e := downloadItem.DoAction(ctx); e == nil {
// success
f.fileInProcessQueue.Remove(downloadItem.syncItem)
f.doPluginCallback(downloadItem.syncItem, "success")
} else {
// retry?
f.fileInProcessQueue.Remove(downloadItem.syncItem)
f.doPluginCallback(downloadItem.syncItem, "fail")
}
downloadWaitGroup.Done()
}()
@@ -838,9 +863,11 @@ func (f *FileActionTaskManager) fileActionTaskExecutor(ctx context.Context) {
if e := deleteLocalItem.DoAction(ctx); e == nil {
// success
f.fileInProcessQueue.Remove(deleteLocalItem.syncItem)
f.doPluginCallback(deleteLocalItem.syncItem, "success")
} else {
// retry?
f.fileInProcessQueue.Remove(deleteLocalItem.syncItem)
f.doPluginCallback(deleteLocalItem.syncItem, "fail")
}
localFileWaitGroup.Done()
}()
@@ -858,9 +885,11 @@ func (f *FileActionTaskManager) fileActionTaskExecutor(ctx context.Context) {
if e := deletePanItem.DoAction(ctx); e == nil {
// success
f.fileInProcessQueue.Remove(deletePanItem.syncItem)
f.doPluginCallback(deletePanItem.syncItem, "success")
} else {
// retry?
f.fileInProcessQueue.Remove(deletePanItem.syncItem)
f.doPluginCallback(deletePanItem.syncItem, "fail")
}
panFileWaitGroup.Done()
}()
@@ -878,9 +907,11 @@ func (f *FileActionTaskManager) fileActionTaskExecutor(ctx context.Context) {
if e := createLocalFolderItem.DoAction(ctx); e == nil {
// success
f.fileInProcessQueue.Remove(createLocalFolderItem.syncItem)
f.doPluginCallback(createLocalFolderItem.syncItem, "success")
} else {
// retry?
f.fileInProcessQueue.Remove(createLocalFolderItem.syncItem)
f.doPluginCallback(createLocalFolderItem.syncItem, "fail")
}
localFileWaitGroup.Done()
}()
@@ -898,9 +929,11 @@ func (f *FileActionTaskManager) fileActionTaskExecutor(ctx context.Context) {
if e := createPanFolderItem.DoAction(ctx); e == nil {
// success
f.fileInProcessQueue.Remove(createPanFolderItem.syncItem)
f.doPluginCallback(createPanFolderItem.syncItem, "success")
} else {
// retry?
f.fileInProcessQueue.Remove(createPanFolderItem.syncItem)
f.doPluginCallback(createPanFolderItem.syncItem, "fail")
}
panFileWaitGroup.Done()
}()
@@ -920,6 +953,51 @@ func (f *FileActionTaskManager) fileActionTaskExecutor(ctx context.Context) {
}
}
func (f *FileActionTaskManager) doPluginCallback(syncFile *SyncFileItem, actionResult string) bool {
// 插件回调
var pluginParam *plugins.SyncFileFinishParams
if syncFile.Action == SyncFileActionUpload ||
syncFile.Action == SyncFileActionCreatePanFolder ||
syncFile.Action == SyncFileActionDeletePan {
file := syncFile.LocalFile
pluginParam = &plugins.SyncFileFinishParams{
Action: string(syncFile.Action),
ActionResult: actionResult,
DriveId: syncFile.DriveId,
FileName: file.FileName,
FilePath: syncFile.getPanFileFullPath(),
FileSha1: file.Sha1Hash,
FileSize: file.FileSize,
FileType: file.FileType,
FileUpdatedAt: file.UpdatedAt,
}
} else if syncFile.Action == SyncFileActionDownload ||
syncFile.Action == SyncFileActionCreateLocalFolder ||
syncFile.Action == SyncFileActionDeleteLocal {
file := syncFile.PanFile
pluginParam = &plugins.SyncFileFinishParams{
Action: string(syncFile.Action),
ActionResult: actionResult,
DriveId: syncFile.DriveId,
FileName: file.FileName,
FilePath: syncFile.getLocalFileFullPath(),
FileSha1: file.Sha1Hash,
FileSize: file.FileSize,
FileType: file.FileType,
FileUpdatedAt: file.UpdatedAt,
}
} else {
return false
}
f.pluginMutex.Lock()
defer f.pluginMutex.Unlock()
if er := f.plugin.SyncFileFinishCallback(plugins.GetContext(f.panUser), pluginParam); er == nil {
return true
}
return false
}
// getRelativePath 获取文件的相对路径
func (l *localFileSet) getRelativePath(localPath string) string {
localPath = strings.ReplaceAll(localPath, "\\", "/")
@@ -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()
+2
View File
@@ -29,6 +29,8 @@ type (
Name string `json:"name"`
// Id 任务ID
Id string `json:"id"`
// UserId 账号ID
UserId string `json:"userId"`
// DriveId 网盘ID,目前支持文件网盘
DriveId string `json:"-"`
// LocalFolderPath 本地目录
+15
View File
@@ -5,6 +5,7 @@ import (
"fmt"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/aliyunpan/internal/config"
"github.com/tickstep/aliyunpan/internal/log"
"github.com/tickstep/aliyunpan/internal/utils"
"github.com/tickstep/library-go/logger"
"io/ioutil"
@@ -29,6 +30,9 @@ type (
// 本地文件修改检测间隔
LocalFileModifiedCheckIntervalSec int
// 文件记录器
FileRecorder *log.FileRecorder
}
// SyncTaskManager 同步任务管理器
@@ -137,6 +141,17 @@ func (m *SyncTaskManager) Start(tasks []*SyncTask, step TaskStep) (bool, error)
if len(task.Id) == 0 {
task.Id = utils.UuidStr()
}
// check userId
if len(task.UserId) > 0 {
if task.UserId != m.PanUser.UserId {
// not this user task, skip
logger.Verboseln("skip sync task: " + task.NameLabel())
continue
}
} else {
task.UserId = m.PanUser.UserId
}
// check pan path
if !utils.IsPanAbsPath(task.PanFolderPath) {
task.PanFolderPath = "/" + task.PanFolderPath
+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",
+27
View File
@@ -20,9 +20,11 @@ import (
"fmt"
jsoniter "github.com/json-iterator/go"
uuid "github.com/satori/go.uuid"
"github.com/tickstep/aliyunpan-api/aliyunpan"
"github.com/tickstep/library-go/ids"
"io"
"io/ioutil"
"math"
"net/http/cookiejar"
"net/url"
"os"
@@ -254,3 +256,28 @@ func IsLocalAbsPath(filePath string) bool {
func IsPanAbsPath(filePath string) bool {
return path.IsAbs(filePath)
}
// IsExcludeFile 是否是指定排除的文件
func IsExcludeFile(filePath string, excludeNames *[]string) bool {
if excludeNames == nil || len(*excludeNames) == 0 {
return false
}
for _, pattern := range *excludeNames {
fileName := path.Base(strings.ReplaceAll(filePath, "\\", "/"))
m, _ := regexp.MatchString(pattern, fileName)
if m {
return true
}
}
return false
}
// ResizeUploadBlockSize 自动调整分片大小,方便支持极大单文件上传。返回新的分片大小
func ResizeUploadBlockSize(fileSize, defaultBlockSize int64) int64 {
if (aliyunpan.MaxPartNum * defaultBlockSize) > fileSize {
return defaultBlockSize
}
sizeOfMB := int64(math.Ceil(float64(fileSize) / float64(aliyunpan.MaxPartNum) / 1024.0))
return sizeOfMB * 1024
}
+12
View File
@@ -39,3 +39,15 @@ func TestIsAbsPath_ReturnTrue(t *testing.T) {
func TestIsAbsPath_ReturnFalse(t *testing.T) {
fmt.Println(IsLocalAbsPath("my\\folder\\test"))
}
func TestResizeUploadBlockSize_ReturnDefaultBlockSize(t *testing.T) {
MB := int64(1024 * 1024) // 1048576
fileSize := int64(1073741824) // 90GB
fmt.Println(ResizeUploadBlockSize(fileSize, 10*MB)) // 10485760 = 10240KB
}
func TestResizeUploadBlockSize_ReturnNewBlockSize(t *testing.T) {
MB := int64(1024 * 1024) // 1048576
fileSize := int64(107374182400) // 100GB
fmt.Println(ResizeUploadBlockSize(fileSize, 10*MB)) // 10737664 = 10486KB
}
+7
View File
@@ -0,0 +1,7 @@
package filelocker
import "errors"
var (
ErrTimeout = errors.New("timeout")
)
+25
View File
@@ -0,0 +1,25 @@
package filelocker
import (
"os"
)
const (
lockExt = ".lock"
)
type (
FileLocker struct {
FilePath string
LockFilePath string
lockFile *os.File
}
)
func NewFileLocker(path string) *FileLocker {
return &FileLocker{
FilePath: path,
LockFilePath: path + lockExt,
lockFile: nil,
}
}
+25
View File
@@ -0,0 +1,25 @@
package filelocker
import (
"fmt"
"log"
"testing"
"time"
)
func TestFlocker(t *testing.T) {
// lock file first time - success
locker := NewFileLocker("D:\\smb\\feny\\goprojects\\dev\\aliyunpan")
e := LockFile(locker, 0755, true, 5*time.Second)
fmt.Println(e)
// lock file again - fail
//time.Sleep(5 * time.Second)
//e = flock(locker, 0755, true, 5*time.Second)
//fmt.Println(e)
// Unlock the file.
if err := UnlockFile(locker); err != nil {
log.Printf("funlock error: %s", err)
}
}
+57
View File
@@ -0,0 +1,57 @@
package filelocker
import (
"os"
"syscall"
"time"
)
// LockFile acquires an advisory lock on a file descriptor.
func LockFile(locker *FileLocker, mode os.FileMode, exclusive bool, timeout time.Duration) error {
f, err := os.OpenFile(locker.LockFilePath, os.O_CREATE, mode)
if err != nil {
return err
}
locker.lockFile = f
var t time.Time
for {
// If we're beyond our timeout then return an error.
// This can only occur after we've attempted a flock once.
if t.IsZero() {
t = time.Now()
} else if timeout > 0 && time.Since(t) > timeout {
return ErrTimeout
}
var lock syscall.Flock_t
lock.Start = 0
lock.Len = 0
lock.Pid = 0
lock.Whence = 0
lock.Pid = 0
if exclusive {
lock.Type = syscall.F_WRLCK
} else {
lock.Type = syscall.F_RDLCK
}
err := syscall.FcntlFlock(locker.lockFile.Fd(), syscall.F_SETLK, &lock)
if err == nil {
return nil
} else if err != syscall.EAGAIN {
return err
}
// Wait for a bit and try again.
time.Sleep(50 * time.Millisecond)
}
}
// UnlockFile releases an advisory lock on a file descriptor.
func UnlockFile(locker *FileLocker) error {
var lock syscall.Flock_t
lock.Start = 0
lock.Len = 0
lock.Type = syscall.F_UNLCK
lock.Whence = 0
return syscall.FcntlFlock(uintptr(locker.lockFile.Fd()), syscall.F_SETLK, &lock)
}
+50
View File
@@ -0,0 +1,50 @@
//go:build !windows && !plan9 && !solaris
// +build !windows,!plan9,!solaris
package filelocker
import (
"os"
"syscall"
"time"
)
// LockFile acquires an advisory lock on a file descriptor.
func LockFile(locker *FileLocker, mode os.FileMode, exclusive bool, timeout time.Duration) error {
f, err := os.OpenFile(locker.LockFilePath, os.O_CREATE, mode)
if err != nil {
return err
}
locker.lockFile = f
var t time.Time
for {
// If we're beyond our timeout then return an error.
// This can only occur after we've attempted a flock once.
if t.IsZero() {
t = time.Now()
} else if timeout > 0 && time.Since(t) > timeout {
return ErrTimeout
}
flag := syscall.LOCK_SH
if exclusive {
flag = syscall.LOCK_EX
}
// Otherwise attempt to obtain an exclusive lock.
err := syscall.Flock(int(locker.lockFile.Fd()), flag|syscall.LOCK_NB)
if err == nil {
return nil
} else if err != syscall.EWOULDBLOCK {
return err
}
// Wait for a bit and try again.
time.Sleep(50 * time.Millisecond)
}
}
// UnlockFile releases an advisory lock on a file descriptor.
func UnlockFile(locker *FileLocker) error {
return syscall.Flock(int(locker.lockFile.Fd()), syscall.LOCK_UN)
}
+86
View File
@@ -0,0 +1,86 @@
package filelocker
import (
"os"
"syscall"
"time"
"unsafe"
)
// LockFileEx code derived from golang build filemutex_windows.go @ v1.5.1
var (
modkernel32 = syscall.NewLazyDLL("kernel32.dll")
procLockFileEx = modkernel32.NewProc("LockFileEx")
procUnlockFileEx = modkernel32.NewProc("UnlockFileEx")
)
const (
// see https://msdn.microsoft.com/en-us/library/windows/desktop/aa365203(v=vs.85).aspx
flagLockExclusive = 2
flagLockFailImmediately = 1
// see https://msdn.microsoft.com/en-us/library/windows/desktop/ms681382(v=vs.85).aspx
errLockViolation syscall.Errno = 0x21
)
func lockFileEx(h syscall.Handle, flags, reserved, locklow, lockhigh uint32, ol *syscall.Overlapped) (err error) {
r, _, err := procLockFileEx.Call(uintptr(h), uintptr(flags), uintptr(reserved), uintptr(locklow), uintptr(lockhigh), uintptr(unsafe.Pointer(ol)))
if r == 0 {
return err
}
return nil
}
func unlockFileEx(h syscall.Handle, reserved, locklow, lockhigh uint32, ol *syscall.Overlapped) (err error) {
r, _, err := procUnlockFileEx.Call(uintptr(h), uintptr(reserved), uintptr(locklow), uintptr(lockhigh), uintptr(unsafe.Pointer(ol)), 0)
if r == 0 {
return err
}
return nil
}
// LockFile acquires an advisory lock on a file descriptor.
func LockFile(locker *FileLocker, mode os.FileMode, exclusive bool, timeout time.Duration) error {
// Create a separate lock file on windows because a process
// cannot share an exclusive lock on the same file. This is
// needed during Tx.WriteTo().
f, err := os.OpenFile(locker.LockFilePath, os.O_CREATE, mode)
if err != nil {
return err
}
locker.lockFile = f
var t time.Time
for {
// If we're beyond our timeout then return an error.
// This can only occur after we've attempted a flock once.
if t.IsZero() {
t = time.Now()
} else if timeout > 0 && time.Since(t) > timeout {
return ErrTimeout
}
var flag uint32 = flagLockFailImmediately
if exclusive {
flag |= flagLockExclusive
}
err := lockFileEx(syscall.Handle(locker.lockFile.Fd()), flag, 0, 1, 0, &syscall.Overlapped{})
if err == nil {
return nil
} else if err != errLockViolation {
return err
}
// Wait for a bit and try again.
time.Sleep(50 * time.Millisecond)
}
}
// UnlockFile releases an advisory lock on a file descriptor.
func UnlockFile(locker *FileLocker) error {
err := unlockFileEx(syscall.Handle(locker.lockFile.Fd()), 0, 1, 0, &syscall.Overlapped{})
locker.lockFile.Close()
os.Remove(locker.LockFilePath)
return err
}
+41 -10
View File
@@ -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.2"
Version = "v0.2.7"
// 命令历史文件
historyFilePath = filepath.Join(config.GetConfigDir(), "aliyunpan_command_history.txt")
@@ -75,16 +76,16 @@ func init() {
}
func checkLoginExpiredAndRelogin() {
cmder.ReloadConfigFunc(nil)
command.ReloadConfigFunc(nil)
activeUser := config.Config.ActiveUser()
if activeUser == nil || activeUser.UserId == "" {
// maybe expired, try to login
cmder.TryLogin()
command.TryLogin()
} else {
// 刷新过期Token并保存到配置文件
command.RefreshTokenInNeed(activeUser)
command.RefreshTokenInNeed(activeUser, config.Config.DeviceName)
}
cmder.SaveConfigFunc(nil)
command.SaveConfigFunc(nil)
}
func main() {
@@ -97,7 +98,7 @@ func main() {
go func() {
for {
// Token刷新进程,不管是CLI命令行模式,还是直接命令模式,本刷新任务都会执行
time.Sleep(time.Duration(5) * time.Minute)
time.Sleep(time.Duration(1) * time.Minute)
//time.Sleep(time.Duration(5) * time.Second)
checkLoginExpiredAndRelogin()
}
@@ -109,7 +110,7 @@ func main() {
app.Name = "aliyunpan"
app.Version = Version
app.Author = "tickstep/aliyunpan: https://github.com/tickstep/aliyunpan"
app.Copyright = "(c) 2021-2022 tickstep."
app.Copyright = "(c) 2021-2023 tickstep."
app.Usage = "阿里云盘客户端 for " + runtime.GOOS + "/" + runtime.GOARCH
app.Description = `aliyunpan 是一款阿里云盘命令行客户端工具, 为操作阿里云盘, 提供实用功能。
支持webdav文件协议,可以让阿里云盘变身为webdav协议的文件服务器,用于挂载作为Windows、Linux、Mac系统的磁盘进行使用。
@@ -220,6 +221,10 @@ func main() {
targetPath string
)
if activeUser == nil {
return
}
if !closed {
targetPath = lineArgs[numArgs-1]
escaper.EscapeStringsByRuneFunc(lineArgs[:numArgs-1], runeFunc) // 转义
@@ -301,7 +306,7 @@ func main() {
fmt.Printf("提示: 输入 help 获取帮助.\n")
// check update
cmder.ReloadConfigFunc(c)
command.ReloadConfigFunc(c)
if config.Config.UpdateCheckInfo.LatestVer != "" {
if utils.ParseVersionNum(config.Config.UpdateCheckInfo.LatestVer) > utils.ParseVersionNum(config.AppVersion) {
fmt.Printf("\n当前的软件版本为:%s, 现在有新版本 %s 可供更新,强烈推荐进行更新!(可以输入 update 命令进行更新)\n\n",
@@ -322,7 +327,7 @@ func main() {
config.Config.UpdateCheckInfo.CheckTime = nowTime
// save
cmder.SaveConfigFunc(c)
command.SaveConfigFunc(c)
}
}()
@@ -333,7 +338,7 @@ func main() {
)
if activeUser == nil {
activeUser = cmder.TryLogin()
activeUser = command.TryLogin()
}
if activeUser != nil && activeUser.Nickname != "" {
@@ -450,6 +455,9 @@ func main() {
// 下载文件/目录 download
command.CmdDownload(),
// 获取文件下载链接
command.CmdLocateUrl(),
// 导出文件/目录元数据 export
//command.CmdExport(),
@@ -593,6 +601,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": 2,
"Patch": 7,
"Build": 0
},
"ProductVersion": {
"Major": 0,
"Minor": 2,
"Patch": 2,
"Patch": 7,
"Build": 0
},
"FileFlagsMask": "3f",
@@ -22,14 +22,14 @@
"Comments": "",
"CompanyName": "tickstep",
"FileDescription": "阿里云盘客户端",
"FileVersion": "v0.2.2",
"FileVersion": "v0.2.7",
"InternalName": "",
"LegalCopyright": "© 2021-2022 tickstep.",
"LegalCopyright": "© 2021-2023 tickstep.",
"LegalTrademarks": "",
"OriginalFilename": "",
"PrivateBuild": "",
"ProductName": "aliyunpan",
"ProductVersion": "v0.2.2",
"ProductVersion": "v0.2.7",
"SpecialBuild": ""
},
"VarFileInfo": {