mirror of
https://gitee.com/zhangsken/aliyunpan.git
synced 2026-10-16 10:29:42 +08:00
Compare commits
82 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d05d19acd8 | |||
| 5179f914ed | |||
| 2feaa3cf82 | |||
| cedd43d402 | |||
| e62bdbb9f1 | |||
| acc408f0a0 | |||
| db28ea6732 | |||
| a9bc1076a5 | |||
| e20ea479c4 | |||
| 42f044ba38 | |||
| 88f1ff2688 | |||
| ca09039e48 | |||
| cf38ad51fd | |||
| 30cbced50c | |||
| 0946939325 | |||
| da13b046b8 | |||
| 268e4a30d1 | |||
| a0ca0f8089 | |||
| 2d99a39606 | |||
| 2965179ae8 | |||
| 0565482121 | |||
| 1822d3c2b2 | |||
| 6672aa4cd4 | |||
| c69a27c9b1 | |||
| 4feacbcdbf | |||
| 1916dda8f8 | |||
| 8b5eb14eb6 | |||
| d3d9690d55 | |||
| 89a6ef326c | |||
| 35dfb35429 | |||
| 3c7ebb5f62 | |||
| f796f40664 | |||
| 0d9700c84c | |||
| 09ee162ba4 | |||
| ff0e3dbfa9 | |||
| 4c5f45e5ab | |||
| 02326b0bd7 | |||
| 350fec4275 | |||
| 70a14dd1a6 | |||
| 23dad72914 | |||
| 9fa547ddc8 | |||
| 2898037ec5 | |||
| 70c14fd66f | |||
| 8913597716 | |||
| 9b276f2caf | |||
| e8bda159fa | |||
| e1ac6a95b9 | |||
| cef9c6c7c2 | |||
| 17e8e182ee | |||
| 4b9ded5b9e | |||
| f939f515f4 | |||
| 0e0486914c | |||
| a86a464e67 | |||
| c932e3a231 | |||
| f79645bfa7 | |||
| 7786dc6c40 | |||
| d7b4d0c846 | |||
| e669063be9 | |||
| e63ba4478c | |||
| 23617d810b | |||
| 3d87c6244f | |||
| 4f7941caf5 | |||
| 095d06d99c | |||
| 7fbe082e58 | |||
| 34a6242bde | |||
| 71df114d8a | |||
| 622984ee26 | |||
| 04ec8658e4 | |||
| 6f0d03bd58 | |||
| 94c24ebf16 | |||
| fb07969bb1 | |||
| 9f83bad7d6 | |||
| 91b1e724fa | |||
| 787d3e1099 | |||
| 87f951b5f5 | |||
| fdafcaed42 | |||
| 9bef162dad | |||
| 6b638d7817 | |||
| ce6d0266bf | |||
| acdc2c93ee | |||
| e9702ea449 | |||
| 212a8ef764 |
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: 问题反馈
|
||||
about: 如果你在使用过程中发现问题,请使用该模板进行反馈。
|
||||
labels: Bug
|
||||
---
|
||||
|
||||
### Ⅰ. 使用环境
|
||||
1. 操作系统:(Linux? macOS? Windows?)
|
||||
2. aliyunpan版本号:(v0.3.6? v0.3.7?...)
|
||||
|
||||
### Ⅱ. 问题描述
|
||||
|
||||
|
||||
### Ⅲ. 期望的结果
|
||||
|
||||
|
||||
### Ⅳ. 如何复现问题
|
||||
<!--
|
||||
如果该问题是必现的,请帮忙整理复现步骤。
|
||||
如果不是必现的,请尽可能说明问题出现之前你的操作步骤,越详细越好。
|
||||
-->
|
||||
1. xxx
|
||||
2. xxx
|
||||
3. xxx
|
||||
|
||||
### Ⅴ. 请提供相关的错误日志
|
||||
<!-- 最好能提供debug日志。如何开启debug日志请参考README文档。 -->
|
||||
```log
|
||||
(日志请贴在这里。如果日志很多,可以以附件的形式上传)
|
||||
|
||||
```
|
||||
@@ -0,0 +1,15 @@
|
||||
---
|
||||
name: 提新需求
|
||||
about: 如果你想提出一个新需求,新功能建议,请使用此模板。
|
||||
labels: Feature Request
|
||||
---
|
||||
|
||||
### Ⅰ. 你当前使用环境
|
||||
1. 操作系统:(Linux? macOS? Windows?)
|
||||
2. aliyunpan版本号:(v0.3.6? v0.3.7?...)
|
||||
|
||||
### Ⅱ. 请描述你想要的新功能
|
||||
<!-- 请简单描述你希望的新功能,例如:"upload上传命令添加一个xxx参数,用于实现xxx功能。" -->
|
||||
|
||||
### Ⅲ. 请描述新功能的使用场景
|
||||
<!-- 可以讲讲你对新功能的看法,为什么需要增加这个功能,这个新功能在哪些场景下会使用。 -->
|
||||
@@ -9,12 +9,13 @@
|
||||
5. [上传](docs/manual.md#上传文件目录)本地文件, 支持多个文件或目录上传,支持排除指定文件夹/文件(正则表达式)功能。支持软链接(符号链接)文件。
|
||||
6. [同步备份功能](docs/manual.md#同步备份功能)支持备份本地文件到云盘,备份云盘文件到本地,双向同步备份保持本地文件和网盘文件同步。常用于嵌入式或者NAS等设备,支持docker镜像部署。
|
||||
7. 命令和文件路径输入支持Tab键自动补全,路径支持通配符匹配模式
|
||||
8. 支持[JavaScript插件](docs/manual.md#JavaScript插件),你可以按照自己的需要定制上传/下载中关键步骤的行为,最大程度满足自己的个性化需求
|
||||
8. 支持[JavaScript插件](docs/plugin_manual.md#简介),你可以按照自己的需要定制上传/下载中关键步骤的行为,最大程度满足自己的个性化需求
|
||||
9. 支持共享相册的相关操作,支持批量下载相册所有普通照片、实况照片文件到本地
|
||||
10. 支持多用户联合下载功能,对下载速度有极致追求的用户可以尝试使用该选项。详情请查看文档[多用户联合下载](docs/manual.md#多用户联合下载)
|
||||
|
||||
# 阿里云盘VIP会员推荐码
|
||||
如果大家有打算开通阿里云盘VIP会员,可以使用阿里云盘APP扫描我们的推荐码进行开通。
|
||||
如果大家有打算开通阿里云盘VIP会员,可以使用阿里云盘APP扫描下面的优惠推荐码进行开通。
|
||||
注意:您需要开通【三方应用权益包】,这样使用本程序下载才能加速,否则下载无法提速。
|
||||

|
||||
|
||||
# 目录
|
||||
@@ -65,9 +66,9 @@
|
||||
|
||||
参考例子:
|
||||
```shell
|
||||
wget https://github.com/tickstep/aliyunpan/releases/download/v0.3.6/aliyunpan-v0.3.6-linux-amd64.zip
|
||||
unzip aliyunpan-v0.3.6-linux-amd64.zip
|
||||
cd aliyunpan-v0.3.6-linux-amd64
|
||||
wget https://github.com/tickstep/aliyunpan/releases/download/v0.4.0/aliyunpan-v0.4.0-linux-amd64.zip
|
||||
unzip aliyunpan-v0.4.0-linux-amd64.zip
|
||||
cd aliyunpan-v0.4.0-linux-amd64
|
||||
./aliyunpan
|
||||
```
|
||||
|
||||
@@ -106,7 +107,7 @@ winget source update
|
||||
```
|
||||
安装
|
||||
```powershell
|
||||
winget install tickstep.aliyunpan --silent
|
||||
winget install tickstep.aliyunpan --source winget --silent
|
||||
|
||||
```
|
||||
|
||||
@@ -125,7 +126,7 @@ winget install tickstep.aliyunpan --silent
|
||||

|
||||
|
||||
```
|
||||
docker run -d --name=aliyunpan-sync --restart=always -v "<your aliyunpan_config.json>:/home/app/config/aliyunpan_config.json" -v "<your local dir>:/home/app/data" -e ALIYUNPAN_PAN_DIR="<your drive pan dir>" -e ALIYUNPAN_SYNC_MODE="upload" -e ALIYUNPAN_SYNC_POLICY="increment" -e ALIYUNPAN_SYNC_DRIVE="backup" -e ALIYUNPAN_SYNC_LOG="true" tickstep/aliyunpan-sync:v0.3.6
|
||||
docker run -d --name=aliyunpan-sync --restart=always -v "<your aliyunpan_config.json>:/home/app/config/aliyunpan_config.json" -v "<your local dir>:/home/app/data" -e ALIYUNPAN_PAN_DIR="<your drive pan dir>" -e ALIYUNPAN_SYNC_MODE="upload" -e ALIYUNPAN_SYNC_POLICY="increment" -e ALIYUNPAN_SYNC_DRIVE="backup" -e ALIYUNPAN_SYNC_LOG="true" tickstep/aliyunpan-sync:v0.4.0
|
||||
|
||||
<your aliyunpan_config.json>: 用户已经登录成功并保存好的aliyunpan_config.json凭据文件
|
||||
<your local dir>:本地目标目录,绝对路径,例如:/tickstep/Documents/设计文档
|
||||
@@ -240,7 +241,7 @@ aliyunpan:/ tickstep$ ls
|
||||
```
|
||||
|
||||
### 下载文件
|
||||
通过 `aliyunpan config set -savedir <savedir>` 可以自定义下载文件保存的目录。
|
||||
如果需要更改默认下载目录路径,可以通过 `aliyunpan config set -savedir <savedir>` 自定义下载文件保存的目录。
|
||||
```shell
|
||||
aliyunpan:/ tickstep$ download IMG_0106.JPG
|
||||
|
||||
@@ -260,7 +261,25 @@ aliyunpan:/ tickstep$ download IMG_0106.JPG
|
||||
[1] 检验文件有效性成功: /root/Downloads/4d001d48564f43b..62874f04bbe6/IMG_0106.JPG
|
||||
|
||||
下载结束, 时间: 4秒, 数据总量: 1.48MB
|
||||
aliyunpan:/ tickstep$
|
||||
```
|
||||
或者使用UI面板展示下载进度(使用-ui选项),更加直观和友好。
|
||||
```shell
|
||||
aliyunpan:/ tickstep$ download /photo -ui
|
||||
┌──────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│总速度: 341.59 KB/s | 文件: 0/7 | 失败: 0 | 状态: 下载中 | 已用时间: 9s | 剩余时间: 51s │
|
||||
│总进度: [===> ] 7.4% (896.00KB/11.83MB) │
|
||||
│──────────────────────────────────────────────────────────────────────────────────────────│
|
||||
│ 1. IMG_0005.JPG [============> ] 66.9% 258.42KB/s 3s │
|
||||
│ 2. IMG_0007.JPG [============> ] 67.3% 258.54KB/s 3s │
|
||||
│ 3. IMG_0009.JPG [ ] 0.0% 等待中 - │
|
||||
│──────────────────────────────────────────────────────────────────────────────────────────│
|
||||
│[10:39:24] [0] 当前文件下载最大并发量为: 2, 单文件下载分片线程数为: 1, 下载缓存为: 64.00KB │
|
||||
│[10:39:27] [2] 加入下载队列: /photo/p/IMG_0007.JPG │
|
||||
│[10:39:27] [2] ---- 文件ID: 67dfe476a7573e76a2034626bd013f36581296ed 文件名: IMG_0007.... │
|
||||
│[10:39:27] [3] 准备下载: /photo/p/IMG_0007.JPG │
|
||||
│[10:39:27] [2] 将会下载到路径: /Users/tickstep/Downloads/4d001d48564f43b3bc5662874f04bb... │
|
||||
│[10:39:28] [2] 下载开始 │
|
||||
└──────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 上传文件
|
||||
@@ -280,9 +299,28 @@ aliyunpan:/ tickstep$ upload /Users/tickstep/Downloads/apt.zip /tmp
|
||||
|
||||
上传结束, 时间: 18秒, 数据总量: 21.00MB
|
||||
```
|
||||
或者使用UI面板展示上传进度(使用-ui选项),更加直观和友好。
|
||||
```shell
|
||||
aliyunpan:/ tickstep$ upload /Users/tickstep/Downloads /photo -ui
|
||||
┌─────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
│总速度: 3.62 MB/s | 文件: 9/14 | 失败: 0 | 状态: 上传中 | 已用时间: 12s | 剩余时间: 17s │
|
||||
│总进度: [==========================> ] 51.7% (68.56MB/132.73MB) │
|
||||
│─────────────────────────────────────────────────────────────────────────────────────────│
|
||||
│ 1. System Design Interview 2.zip [====> ] 26.2% 3.25MB/s 18s │
|
||||
│ 2. ips_20260228.txt [ ] 0.0% 0 B/s - │
|
||||
│ 3. neko-agent [ ] 0.0% 0 B/s - │
|
||||
│─────────────────────────────────────────────────────────────────────────────────────────│
|
||||
│[11:14:49] [9] 2026-03-02 11:14:26 正在计算文件SHA1: /Users/tickstep/Downloads/test/da... │
|
||||
│[11:14:49] [13] 2026-03-02 11:14:26 正在检测和创建云盘文件夹: /photo/p2/test/output │
|
||||
│[11:14:49] [9] 2026-03-02 11:14:26 检测秒传中, 请稍候... │
|
||||
│[11:14:49] [9] 2026-03-02 11:14:26 秒传成功, 保存到网盘路径: /photo/p2/test/data/ips_2... │
|
||||
│[11:14:49] [9] 2026-03-02 11:14:26 文件上传结果: 成功! 耗时 12秒 │
|
||||
│[11:14:49] 完成: /Users/tickstep/Downloads/test/data/ips_20260226.txt │
|
||||
└─────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 同步备份文件
|
||||
同步备份功能,支持备份本地文件到云盘,备份云盘文件到本地,双向同步备份三种模式。支持JavaScript插件对备份文件进行过滤。
|
||||
同步备份功能,支持备份本地文件到云盘,备份云盘文件到本地两种模式。支持JavaScript插件对备份文件进行过滤。
|
||||
|
||||
例如:将本地目录 `/tickstep/Documents/设计文档` 中的文件备份上传到云盘目录 `/备份盘/我的文档`
|
||||
```shell
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<assemblyIdentity version="0.3.6.0" processorArchitecture="*" name="com.tickstep.aliyunpan" type="win32"/>
|
||||
<assemblyIdentity version="0.4.0.0" processorArchitecture="*" name="com.tickstep.aliyunpan" type="win32"/>
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||
|
||||
@@ -12,31 +12,33 @@
|
||||
// 参数说明
|
||||
// context - 当前调用的上下文信息
|
||||
// {
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "albumDriveId": "29519122"
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "resourceDriveId": "29519122"
|
||||
// }
|
||||
// appName - 应用名称,当前固定为aliyunpan
|
||||
// version - 版本号
|
||||
// userId - 当前登录用户的ID
|
||||
// nickname - 用户昵称
|
||||
// fileDriveId - 用户文件网盘ID
|
||||
// albumDriveId - 用户相册网盘ID
|
||||
// fileDriveId - 用户备份网盘ID
|
||||
// resourceDriveId - 用户资源网盘ID
|
||||
//
|
||||
// params - 文件下载前参数
|
||||
// {
|
||||
// "driveId": "19519221",
|
||||
// "driveFileName": "token.bat",
|
||||
// "driveFilePath": "/aliyunpan/Downloads/token.bat",
|
||||
// "driveFileSha1": "08FBE28A5B8791A2F50225E2EC5CEEC3C7955A11",
|
||||
// "driveFileSize": 125330,
|
||||
// "driveFileType": "file",
|
||||
// "driveFileUpdatedAt": "2022-04-14 07:05:12",
|
||||
// "localFilePath": "aliyunpan\\Downloads\\token.bat"
|
||||
// "downloadActionId": "20976308-a026-450d-92b8-16a969fed0c6",
|
||||
// "driveId": "19519221",
|
||||
// "driveFileName": "token.bat",
|
||||
// "driveFilePath": "/aliyunpan/Downloads/token.bat",
|
||||
// "driveFileSha1": "08FBE28A5B8791A2F50225E2EC5CEEC3C7955A11",
|
||||
// "driveFileSize": 125330,
|
||||
// "driveFileType": "file",
|
||||
// "driveFileUpdatedAt": "2022-04-14 07:05:12",
|
||||
// "localFilePath": "aliyunpan\\Downloads\\token.bat"
|
||||
// }
|
||||
// downloadActionId - 下载动作ID。同一次下载动作(download命令)下载的文件,这个ID都是同一个值。注意这个ID不是永久的,每执行一次download命令会生成一个对应ID。
|
||||
// driveId - 网盘ID
|
||||
// driveFileName - 网盘文件名
|
||||
// driveFilePath - 网盘文件绝对完整路径
|
||||
@@ -48,8 +50,8 @@
|
||||
//
|
||||
// 返回值说明
|
||||
// {
|
||||
// "downloadApproved": "yes",
|
||||
// "localFilePath": "newfolder\\token.bat"
|
||||
// "downloadApproved": "yes",
|
||||
// "localFilePath": "newfolder\\token.bat"
|
||||
// }
|
||||
// downloadApproved - 该文件是否下载,yes-确认下载,no-取消下载
|
||||
// localFilePath - 文件保存的本地路径,这个是相对路径,如果为空""代表保持原本的目标路径。
|
||||
@@ -83,33 +85,37 @@ function downloadFilePrepareCallback(context, params) {
|
||||
// 参数说明
|
||||
// context - 当前调用的上下文信息
|
||||
// {
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "albumDriveId": "29519122"
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "resourceDriveId": "29519122"
|
||||
// }
|
||||
// appName - 应用名称,当前固定为aliyunpan
|
||||
// version - 版本号
|
||||
// userId - 当前登录用户的ID
|
||||
// nickname - 用户昵称
|
||||
// fileDriveId - 用户文件网盘ID
|
||||
// albumDriveId - 用户相册网盘ID
|
||||
// fileDriveId - 用户备份网盘ID
|
||||
// resourceDriveId - 用户资源网盘ID
|
||||
//
|
||||
// params - 文件下载结束参数
|
||||
// {
|
||||
// "driveId": "19519221",
|
||||
// "driveFileName": "token.bat",
|
||||
// "driveFilePath": "/aliyunpan/Downloads/token.bat",
|
||||
// "driveFileSha1": "08FBE28A5B8791A2F50225E2EC5CEEC3C7955A11",
|
||||
// "driveFileSize": 125330,
|
||||
// "driveFileType": "file",
|
||||
// "driveFileUpdatedAt": "2022-04-14 07:05:12",
|
||||
// "downloadActionId": "20976308-a026-450d-92b8-16a969fed0c6",
|
||||
// "driveId": "19519221",
|
||||
// "driveFileId": "65ebb9396d66dfcd9f25414bbdd81594ecb9fcd6",
|
||||
// "driveFileName": "token.bat",
|
||||
// "driveFilePath": "/aliyunpan/Downloads/token.bat",
|
||||
// "driveFileSha1": "08FBE28A5B8791A2F50225E2EC5CEEC3C7955A11",
|
||||
// "driveFileSize": 125330,
|
||||
// "driveFileType": "file",
|
||||
// "driveFileUpdatedAt": "2022-04-14 07:05:12",
|
||||
// "downloadResult": "success",
|
||||
// "localFilePath": "aliyunpan\\Downloads\\token.bat"
|
||||
// "localFilePath": "aliyunpan\\Downloads\\token.bat"
|
||||
// }
|
||||
// downloadActionId - 下载动作ID。同一次下载动作(download命令)下载的文件,这个ID都是同一个值。注意这个ID不是永久的,每执行一次download命令会生成一个对应ID。
|
||||
// driveId - 网盘ID
|
||||
// driveFileId - 网盘文件ID
|
||||
// driveFileName - 网盘文件名
|
||||
// driveFilePath - 网盘文件绝对完整路径
|
||||
// driveFileSize - 网盘文件大小,单位B
|
||||
@@ -124,4 +130,17 @@ function downloadFilePrepareCallback(context, params) {
|
||||
// ------------------------------------------------------------------------------------------
|
||||
function downloadFileFinishCallback(context, params) {
|
||||
console.log(params)
|
||||
|
||||
// // 云盘文件成功下载到本地后,删除云盘的文件
|
||||
// if (params["downloadResult"] == "success") {
|
||||
// if (params["driveFileType"] == "file") {
|
||||
// // 文件下载成功,删除该云盘文件
|
||||
// var userId = context["userId"]
|
||||
// var driveId = params["driveId"]
|
||||
// var driveFileId = params["driveFileId"]
|
||||
// if (PluginUtil.PanFS.deleteFile(userId, driveId, driveFileId)) {
|
||||
// console.log("插件删除云盘文件成功:" + params["driveFilePath"])
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
Executable
+105
@@ -0,0 +1,105 @@
|
||||
// ==========================================================================================
|
||||
// aliyunpan JS插件回调处理函数
|
||||
// 支持 JavaScript ECMAScript 5.1 语言规范
|
||||
//
|
||||
// 更多内容请查看官方文档:https://github.com/tickstep/aliyunpan
|
||||
// ==========================================================================================
|
||||
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// 函数说明:删除文件前的回调函数
|
||||
//
|
||||
// 参数说明
|
||||
// context - 当前调用的上下文信息
|
||||
// {
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "resourceDriveId": "29519122"
|
||||
// }
|
||||
// appName - 应用名称,当前固定为aliyunpan
|
||||
// version - 版本号
|
||||
// userId - 当前登录用户的ID
|
||||
// nickname - 用户昵称
|
||||
// fileDriveId - 用户备份网盘ID
|
||||
// resourceDriveId - 用户资源网盘ID
|
||||
//
|
||||
// params - 删除文件前的调用参数
|
||||
// {
|
||||
// "count": 10,
|
||||
// "items": [{}, {}, ...],
|
||||
// }
|
||||
// count - 文件总数
|
||||
// items - 文件列表,文件详情请查看下面 RemoveFilePrepareItem 定义
|
||||
//
|
||||
// RemoveFilePrepareItem - 文件详情参数
|
||||
// {
|
||||
// "driveId": "19519221",
|
||||
// "driveFileId": "65f6c5161ee4a1b9a02b41399282f281c6e6df21",
|
||||
// "driveFileName": "token.bat",
|
||||
// "driveFilePath": "/aliyunpan/Downloads/token.bat",
|
||||
// "driveFileSize": 125330,
|
||||
// "driveFileType": "file",
|
||||
// "driveFileUpdatedAt": "2025-03-02 10:39:14",
|
||||
// "driveFileCreatedAt": "2025-03-02 10:39:14"
|
||||
// }
|
||||
// driveId - 网盘ID
|
||||
// driveFileId - 网盘文件的ID
|
||||
// driveFileName - 网盘文件名
|
||||
// driveFilePath - 网盘文件绝对完整路径
|
||||
// driveFileSize - 网盘文件大小,单位B
|
||||
// driveFileType - 网盘文件类型,file-文件,folder-文件夹
|
||||
// driveFileUpdatedAt - 网盘文件修改时间
|
||||
// driveFileCreatedAt - 网盘文件创建时间
|
||||
//
|
||||
// 返回值说明
|
||||
// {
|
||||
// "result": [{}, {}, ...],
|
||||
// }
|
||||
// result - 结果列表,每个文件的确认结果,文件详细定义请查看 RemoveFilePrepareResultItem 定义
|
||||
//
|
||||
// RemoveFilePrepareResultItem - 每项文件确认结果
|
||||
// {
|
||||
// "driveId": "19519221",
|
||||
// "driveFileId": "65f6c5161ee4a1b9a02b41399282f281c6e6df21",
|
||||
// "removeApproved": "yes"
|
||||
// }
|
||||
// driveId - 网盘ID
|
||||
// driveFileId - 网盘文件的ID
|
||||
// removeApproved - 该文件是否能删除,yes-确认删除,no-不能删除
|
||||
// ------------------------------------------------------------------------------------------
|
||||
function removeFilePrepareCallback(context, params) {
|
||||
console.log(params)
|
||||
var result = {"result":[]};
|
||||
for(var i = 0; i < params.items.length; i++) {
|
||||
var fileItem = params.items[i]
|
||||
console.log(fileItem);
|
||||
var fileItemApprovedResult = {
|
||||
"driveId": fileItem["driveId"],
|
||||
"driveFileId": fileItem["driveFileId"],
|
||||
"removeApproved": "yes"
|
||||
}
|
||||
|
||||
// 可以通过名称过滤
|
||||
if (fileItem["driveFileType"] == "file") { // 文件
|
||||
if (fileItem["driveFileName"] == "password.key") {
|
||||
fileItemApprovedResult["removeApproved"] = "no"; // 禁止删除password.key文件
|
||||
}
|
||||
} else if (params["driveFileType"] == "folder") { // 文件夹
|
||||
// 对文件夹进行处理
|
||||
}
|
||||
|
||||
// 可以通过时间过滤,例如:保留最近10天的文件
|
||||
// ...
|
||||
|
||||
// 可以通过数量过滤,例如:保留最新5个文件
|
||||
// ...
|
||||
|
||||
// 放入结果列表
|
||||
result["result"].push(fileItemApprovedResult)
|
||||
}
|
||||
// 返回
|
||||
return result;
|
||||
}
|
||||
@@ -12,28 +12,28 @@
|
||||
// 参数说明
|
||||
// context - 当前调用的上下文信息
|
||||
// {
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "albumDriveId": "29519122"
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "resourceDriveId": "29519122"
|
||||
// }
|
||||
// appName - 应用名称,当前固定为aliyunpan
|
||||
// version - 版本号
|
||||
// userId - 当前登录用户的ID
|
||||
// nickname - 用户昵称
|
||||
// fileDriveId - 用户文件网盘ID
|
||||
// albumDriveId - 用户相册网盘ID
|
||||
// fileDriveId - 用户备份网盘ID
|
||||
// resourceDriveId - 用户资源网盘ID
|
||||
//
|
||||
// params - 扫描本地文件前参数
|
||||
// {
|
||||
// "localFilePath": "D:\\Program Files\\aliyunpan\\Downloads\\token.bat",
|
||||
// "localFileName": "token.bat",
|
||||
// "localFileSize": 125330,
|
||||
// "localFileType": "file",
|
||||
// "localFileUpdatedAt": "2022-04-14 07:05:12",
|
||||
// "driveId": "19519221"
|
||||
// "localFilePath": "D:\\Program Files\\aliyunpan\\Downloads\\token.bat",
|
||||
// "localFileName": "token.bat",
|
||||
// "localFileSize": 125330,
|
||||
// "localFileType": "file",
|
||||
// "localFileUpdatedAt": "2022-04-14 07:05:12",
|
||||
// "driveId": "19519221"
|
||||
// }
|
||||
// localFilePath - 本地文件绝对完整路径
|
||||
// localFileName - 本地文件名
|
||||
@@ -44,13 +44,13 @@
|
||||
//
|
||||
// 返回值说明
|
||||
// {
|
||||
// "syncScanLocalApproved": "yes"
|
||||
// "syncScanLocalApproved": "yes"
|
||||
// }
|
||||
// syncScanLocalApproved - 该文件是否确认扫描,yes-允许扫描,no-禁止扫描。
|
||||
// 禁止扫描的文件不会执行后续的动作,例如上传,下载。
|
||||
// ------------------------------------------------------------------------------------------
|
||||
function syncScanLocalFilePrepareCallback(context, params) {
|
||||
console.log(params);
|
||||
console.log(params);
|
||||
var result = {
|
||||
"syncScanLocalApproved": "yes"
|
||||
};
|
||||
@@ -60,7 +60,7 @@ function syncScanLocalFilePrepareCallback(context, params) {
|
||||
result["syncScanLocalApproved"] = "no";
|
||||
}
|
||||
|
||||
// 禁止~$开头文件上传(office暂存临时文件)
|
||||
// 禁止~$开头文件上传(office暂存临时文件)
|
||||
if (params["localFileName"].indexOf("~$") == 0) {
|
||||
result["syncScanLocalApproved"] = "no";
|
||||
}
|
||||
@@ -74,9 +74,9 @@ function syncScanLocalFilePrepareCallback(context, params) {
|
||||
if (params["localFileName"] == "password.key") {
|
||||
result["syncScanLocalApproved"] = "no";
|
||||
}
|
||||
|
||||
// 禁止@eadir文件上传
|
||||
if (params["localFileName"] == "@eadir") {
|
||||
|
||||
// 禁止@eadir文件上传
|
||||
if (params["localFileName"] == "@eadir") {
|
||||
result["syncScanLocalApproved"] = "no";
|
||||
}
|
||||
return result;
|
||||
@@ -89,29 +89,29 @@ function syncScanLocalFilePrepareCallback(context, params) {
|
||||
// 参数说明
|
||||
// context - 当前调用的上下文信息
|
||||
// {
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "albumDriveId": "29519122"
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "resourceDriveId": "29519122"
|
||||
// }
|
||||
// appName - 应用名称,当前固定为aliyunpan
|
||||
// version - 版本号
|
||||
// userId - 当前登录用户的ID
|
||||
// nickname - 用户昵称
|
||||
// fileDriveId - 用户文件网盘ID
|
||||
// albumDriveId - 用户相册网盘ID
|
||||
// fileDriveId - 用户备份网盘ID
|
||||
// resourceDriveId - 用户资源网盘ID
|
||||
//
|
||||
// params - 扫描云盘文件前参数
|
||||
// {
|
||||
// "driveId": "19519221",
|
||||
// "driveFileName": "token.bat",
|
||||
// "driveFilePath": "/aliyunpan/Downloads/token.bat",
|
||||
// "driveFileSha1": "08FBE28A5B8791A2F50225E2EC5CEEC3C7955A11",
|
||||
// "driveFileSize": 125330,
|
||||
// "driveFileType": "file",
|
||||
// "driveFileUpdatedAt": "2022-04-14 07:05:12"
|
||||
// "driveId": "19519221",
|
||||
// "driveFileName": "token.bat",
|
||||
// "driveFilePath": "/aliyunpan/Downloads/token.bat",
|
||||
// "driveFileSha1": "08FBE28A5B8791A2F50225E2EC5CEEC3C7955A11",
|
||||
// "driveFileSize": 125330,
|
||||
// "driveFileType": "file",
|
||||
// "driveFileUpdatedAt": "2022-04-14 07:05:12"
|
||||
// }
|
||||
// driveId - 网盘ID
|
||||
// driveFileName - 网盘文件名
|
||||
@@ -123,7 +123,7 @@ function syncScanLocalFilePrepareCallback(context, params) {
|
||||
//
|
||||
// 返回值说明
|
||||
// {
|
||||
// "syncScanPanApproved": "yes"
|
||||
// "syncScanPanApproved": "yes"
|
||||
// }
|
||||
// syncScanPanApproved - 该文件是否确认扫描,yes-允许扫描,no-禁止扫描。
|
||||
// 禁止扫描的文件不会执行后续的动作,例如上传,下载。
|
||||
@@ -159,41 +159,37 @@ function syncScanPanFilePrepareCallback(context, params) {
|
||||
// 参数说明
|
||||
// context - 当前调用的上下文信息
|
||||
// {
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "albumDriveId": "29519122"
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "resourceDriveId": "29519122"
|
||||
// }
|
||||
// appName - 应用名称,当前固定为aliyunpan
|
||||
// version - 版本号
|
||||
// userId - 当前登录用户的ID
|
||||
// nickname - 用户昵称
|
||||
// fileDriveId - 用户文件网盘ID
|
||||
// albumDriveId - 用户相册网盘ID
|
||||
// fileDriveId - 用户备份网盘ID
|
||||
// resourceDriveId - 用户资源网盘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": "upload",
|
||||
// "actionResult": "success",
|
||||
// "driveId": "19519221",
|
||||
// "driveFileId": "65ebb9396d66dfcd9f25414bbdd81594ecb9fcd6",
|
||||
// "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-创建云盘文件夹
|
||||
// action - 同步动作,download-下载文件,upload-上传文件
|
||||
// actionResult - 同步结果,success-成功,fail-失败
|
||||
// driveId - 网盘ID
|
||||
// driveFileId - 网盘文件ID,只有download模式才会有这个字段
|
||||
// fileName - 文件名称
|
||||
// filePath - 文件完整路径
|
||||
// fileSha1 - 文件SHA1
|
||||
@@ -205,5 +201,66 @@ function syncScanPanFilePrepareCallback(context, params) {
|
||||
// (无)
|
||||
// ------------------------------------------------------------------------------------------
|
||||
function syncFileFinishCallback(context, params) {
|
||||
console.log(params)
|
||||
console.log(params)
|
||||
// 云盘文件成功下载到本地后,删除云盘的文件
|
||||
// if (params["action"] == "download") {
|
||||
// if (params["actionResult"] == "success" && params["fileType"] == "file") {
|
||||
// // 文件下载成功,删除该云盘文件
|
||||
// var userId = context["userId"]
|
||||
// var driveId = params["driveId"]
|
||||
// var driveFileId = params["driveFileId"]
|
||||
// if (PluginUtil.PanFS.deleteFile(userId, driveId, driveFileId)) {
|
||||
// console.println("插件删除云盘文件成功:" + params["filePath"])
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// 函数说明:同步备份-同步任务同步全部文件完成时的回调函数(目前只有"只运行一次备份"的任务模式才有该回调)
|
||||
//
|
||||
// 参数说明
|
||||
// context - 当前调用的上下文信息
|
||||
// {
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "resourceDriveId": "29519122"
|
||||
// }
|
||||
// appName - 应用名称,当前固定为aliyunpan
|
||||
// version - 版本号
|
||||
// userId - 当前登录用户的ID
|
||||
// nickname - 用户昵称
|
||||
// fileDriveId - 用户备份网盘ID
|
||||
// resourceDriveId - 用户资源网盘ID
|
||||
//
|
||||
// params - 任务完成的回调参数
|
||||
// {
|
||||
// "name": "备份相册",
|
||||
// "id": "2e959a34-8435-4773-9a3e-687f2692ddc1",
|
||||
// "userId": "4d001d48564f43b3bc5662874114bbe6",
|
||||
// "driveName": "backup",
|
||||
// "driveId": "19519221",
|
||||
// "localFolderPath": "/Volumes/DataDisk3T/Games/Switch/图库备份",
|
||||
// "panFolderPath": "/备份盘/Switch游戏/Album",
|
||||
// "mode": "upload",
|
||||
// "policy": "upload",
|
||||
// }
|
||||
// name - 任务名称
|
||||
// id - 任务ID
|
||||
// userId - 登录的用户ID
|
||||
// driveName - 网盘名称,backup-备份盘,resource-资源盘
|
||||
// driveId - 目标网盘ID
|
||||
// localFolderPath - 本地目录
|
||||
// panFolderPath - 云盘目录
|
||||
// mode - 备份模式,upload-备份本地文件到云盘,download-备份云盘文件到本地
|
||||
// policy - 备份策略,increment-增量备份,exclusive-排他备份
|
||||
//
|
||||
// 返回值说明
|
||||
// 无
|
||||
// ------------------------------------------------------------------------------------------
|
||||
function syncAllFileFinishCallback(context, params) {
|
||||
console.log(params)
|
||||
}
|
||||
@@ -12,27 +12,27 @@
|
||||
// 参数说明
|
||||
// context - 当前调用的上下文信息
|
||||
// {
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "albumDriveId": "29519122"
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "resourceDriveId": "29519122"
|
||||
// }
|
||||
// appName - 应用名称,当前固定为aliyunpan
|
||||
// version - 版本号
|
||||
// userId - 当前登录用户的ID
|
||||
// nickname - 用户昵称
|
||||
// fileDriveId - 用户文件网盘ID
|
||||
// albumDriveId - 用户相册网盘ID
|
||||
// fileDriveId - 用户备份网盘ID
|
||||
// resourceDriveId - 用户资源网盘ID
|
||||
//
|
||||
// params - Token刷新参数
|
||||
// {
|
||||
// "result": "success",
|
||||
// "message": "ok",
|
||||
// "oldToken": "aa31fcc229c54d5ab6d8bfb17aff3711",
|
||||
// "newToken": "bb31fcc229c54d5ab6d8bfb17aff3722",
|
||||
// "updatedAt": "2022-04-14 07:05:12"
|
||||
// "result": "success",
|
||||
// "message": "ok",
|
||||
// "oldToken": "aa31fcc229c54d5ab6d8bfb17aff3711",
|
||||
// "newToken": "bb31fcc229c54d5ab6d8bfb17aff3722",
|
||||
// "updatedAt": "2022-04-14 07:05:12"
|
||||
// }
|
||||
// result - Token刷新的结果,success-成功,fail-失败
|
||||
// message - 消息说明,如果失败这里会有原因说明
|
||||
|
||||
@@ -12,29 +12,29 @@
|
||||
// 参数说明
|
||||
// context - 当前调用的上下文信息
|
||||
// {
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "albumDriveId": "29519122"
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "resourceDriveId": "29519122"
|
||||
// }
|
||||
// appName - 应用名称,当前固定为aliyunpan
|
||||
// version - 版本号
|
||||
// userId - 当前登录用户的ID
|
||||
// nickname - 用户昵称
|
||||
// fileDriveId - 用户文件网盘ID
|
||||
// albumDriveId - 用户相册网盘ID
|
||||
// fileDriveId - 用户备份网盘ID
|
||||
// resourceDriveId - 用户资源网盘ID
|
||||
//
|
||||
// params - 文件上传前参数
|
||||
// {
|
||||
// "localFilePath": "D:\\Program Files\\aliyunpan\\Downloads\\token.bat",
|
||||
// "localFileName": "token.bat",
|
||||
// "localFileSize": 125330,
|
||||
// "localFileType": "file",
|
||||
// "localFileUpdatedAt": "2022-04-14 07:05:12",
|
||||
// "driveId": "19519221",
|
||||
// "driveFilePath": "aliyunpan/Downloads/token.bat"
|
||||
// "localFilePath": "D:\\Program Files\\aliyunpan\\Downloads\\token.bat",
|
||||
// "localFileName": "token.bat",
|
||||
// "localFileSize": 125330,
|
||||
// "localFileType": "file",
|
||||
// "localFileUpdatedAt": "2022-04-14 07:05:12",
|
||||
// "driveId": "19519221",
|
||||
// "driveFilePath": "aliyunpan/Downloads/token.bat"
|
||||
// }
|
||||
// localFilePath - 本地文件绝对完整路径
|
||||
// localFileName - 本地文件名
|
||||
@@ -46,8 +46,8 @@
|
||||
//
|
||||
// 返回值说明
|
||||
// {
|
||||
// "uploadApproved": "yes",
|
||||
// "driveFilePath": "newfolder/token.bat"
|
||||
// "uploadApproved": "yes",
|
||||
// "driveFilePath": "newfolder/token.bat"
|
||||
// }
|
||||
// uploadApproved - 该文件是否确认上传,yes-允许上传,no-禁止上传
|
||||
// driveFilePath - 文件保存的网盘路径,这个是相对路径,如果为空""代表保持原本的目标路径。
|
||||
@@ -84,31 +84,31 @@ function uploadFilePrepareCallback(context, params) {
|
||||
// 参数说明
|
||||
// context - 当前调用的上下文信息
|
||||
// {
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "albumDriveId": "29519122"
|
||||
// "appName": "aliyunpan",
|
||||
// "version": "v0.1.3",
|
||||
// "userId": "11001d48564f43b3bc5662874f04bb11",
|
||||
// "nickname": "tickstep",
|
||||
// "fileDriveId": "19519111",
|
||||
// "resourceDriveId": "29519122"
|
||||
// }
|
||||
// appName - 应用名称,当前固定为aliyunpan
|
||||
// version - 版本号
|
||||
// userId - 当前登录用户的ID
|
||||
// nickname - 用户昵称
|
||||
// fileDriveId - 用户文件网盘ID
|
||||
// albumDriveId - 用户相册网盘ID
|
||||
// fileDriveId - 用户备份网盘ID
|
||||
// resourceDriveId - 用户资源网盘ID
|
||||
//
|
||||
// params - 文件上传结束参数
|
||||
// {
|
||||
// "localFilePath": "D:\\Program Files\\aliyunpan\\Downloads\\token.bat",
|
||||
// "localFileName": "token.bat",
|
||||
// "localFileSize": 125330,
|
||||
// "localFileType": "file",
|
||||
// "localFileUpdatedAt": "2022-04-14 07:05:12",
|
||||
// "localFileSha1": "08FBE28A5B8791A2F50225E2EC5CEEC3C7955A11",
|
||||
// "uploadResult": "success",
|
||||
// "driveId": "19519221",
|
||||
// "driveFilePath": "/tmp/test/aliyunpan/Downloads/token.bat"
|
||||
// "localFilePath": "D:\\Program Files\\aliyunpan\\Downloads\\token.bat",
|
||||
// "localFileName": "token.bat",
|
||||
// "localFileSize": 125330,
|
||||
// "localFileType": "file",
|
||||
// "localFileUpdatedAt": "2022-04-14 07:05:12",
|
||||
// "localFileSha1": "08FBE28A5B8791A2F50225E2EC5CEEC3C7955A11",
|
||||
// "uploadResult": "success",
|
||||
// "driveId": "19519221",
|
||||
// "driveFilePath": "/tmp/test/aliyunpan/Downloads/token.bat"
|
||||
// }
|
||||
// localFilePath - 本地文件绝对完整路径
|
||||
// localFileName - 本地文件名
|
||||
|
||||
@@ -76,6 +76,7 @@ Pack() {
|
||||
|
||||
cp README.md "$output/$1"
|
||||
cp docs/manual.md "$output/$1"
|
||||
cp docs/plugin_manual.md "$output/$1"
|
||||
cp -rf assets/scripts/* "$output/$1"
|
||||
cp -rf assets/plugin "$output/$1"
|
||||
cp -rf assets/sync_drive "$output/$1"
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
# alpine:3.15.0
|
||||
# alpine:3.21
|
||||
|
||||
# 参数
|
||||
ARG DOCKER_IMAGE_HASH
|
||||
|
||||
FROM alpine@sha256:$DOCKER_IMAGE_HASH
|
||||
#FROM alpine@sha256:$DOCKER_IMAGE_HASH
|
||||
#FROM alpine
|
||||
FROM alpine:$DOCKER_IMAGE_HASH
|
||||
|
||||
LABEL author="tickstep"
|
||||
LABEL email="tickstep@outlook.com"
|
||||
LABEL version="1.0"
|
||||
|
||||
+50
-619
@@ -20,7 +20,6 @@
|
||||
* [创建目录](#创建目录)
|
||||
* [删除文件/目录](#删除文件目录)
|
||||
* [移动文件/目录](#移动文件目录)
|
||||
* [备份盘和资源库之间转存文件](#备份盘和资源库之间转存文件)
|
||||
* [重命名文件/目录](#重命名文件目录)
|
||||
* [分享文件/目录](#分享文件目录)
|
||||
+ [设置分享文件/目录](#设置分享文件目录)
|
||||
@@ -39,17 +38,8 @@
|
||||
+ [Windows后台启动](#Windows后台启动)
|
||||
+ [Docker运行](#Docker运行)
|
||||
* [JavaScript插件](#JavaScript插件)
|
||||
+ [如何使用](#如何使用)
|
||||
+ [JS中内置的函数](#JS中内置的函数)
|
||||
+ [常见场景样例](#常见场景样例)
|
||||
+ [1.禁止特定文件上传](#1.禁止特定文件上传)
|
||||
+ [2.上传文件后删除本地文件](#2.上传文件后删除本地文件)
|
||||
+ [3.下载文件并截断过长的文件名](#3.下载文件并截断过长的文件名)
|
||||
+ [4.上传文件去掉文件名包含的部分字符](#4.上传文件去掉文件名包含的部分字符)
|
||||
+ [5.上传文件时去掉指定目录或者文件](#5.上传文件时去掉指定目录或者文件)
|
||||
+ [6.Token刷新失败发送外部通知](#6.Token刷新失败发送外部通知)
|
||||
* [显示和修改程序配置项](#显示和修改程序配置项)
|
||||
- [常见问题Q&A](#常见问题Q&A)
|
||||
- [常见问题Q&A](#常见问题QA)
|
||||
* [1. 如何开启Debug调试日志](#1-如何开启Debug调试日志)
|
||||
|
||||
# 命令列表及说明
|
||||
@@ -226,18 +216,20 @@ aliyunpan d <网盘文件或目录的路径1> <文件或目录2> <文件或目
|
||||
|
||||
### 可选参数
|
||||
```
|
||||
--ow overwrite, 覆盖已存在的文件
|
||||
--skip skip same name, 跳过已存在的同名文件,即使文件内容不一致(不检查SHA1)
|
||||
--status 输出所有线程的工作状态
|
||||
--save 将下载的文件直接保存到当前工作目录
|
||||
--saveto value 将下载的文件直接保存到指定的目录
|
||||
-x 为文件加上执行权限, (windows系统无效)
|
||||
-p value 指定下载线程数 (default: 0)
|
||||
-l value 指定同时进行下载文件的数量 (default: 0)
|
||||
--retry value 下载失败最大重试次数 (default: 3)
|
||||
--nocheck 下载文件完成后不校验文件
|
||||
--exn value 指定排除的文件夹或者文件的名称,只支持正则表达式。支持排除多个名称,每一个名称就是一个exn参数
|
||||
--md (BETA) Multi-User Download,使用多用户联合下载,可以对单一文件叠加所有登录用户的下载速度
|
||||
--ow overwrite, 覆盖已存在的文件
|
||||
--status 输出所有线程的工作状态
|
||||
--save 将下载的文件直接保存到当前工作目录
|
||||
--saveto value 将下载的文件直接保存到指定的目录
|
||||
-x 为文件加上执行权限, (windows系统无效)
|
||||
-p value parallel,指定同时进行下载文件的数量(取值范围:1 ~ 3) (default: 1)
|
||||
--sp value slice parallel,指定单个文件下载的最大线程(分片)数(取值范围:1 ~ 3) (default: 0)
|
||||
--retry value 下载失败最大重试次数 (default: 3)
|
||||
--nocheck 下载文件完成后不校验文件
|
||||
--np no progress 不展示下载进度条
|
||||
--driveId value 网盘ID
|
||||
--exn value exclude name,指定排除的文件夹或者文件的名称,被排除的文件不会进行下载,只支持正则表达式。支持同时排除多个名称,每一个名称就是一个exn参数
|
||||
--md (BETA) Multi-User Download,使用多用户联合下载,可以对单一文件叠加所有登录用户的下载速度
|
||||
--ui (BETA) 使用UI面板显示下载详情和进度,更加直观和友好
|
||||
```
|
||||
|
||||
|
||||
@@ -252,6 +244,9 @@ aliyunpan d /我的文档/1.mp4
|
||||
|
||||
# 下载 /我的文档 整个目录!!
|
||||
aliyunpan d /我的文档
|
||||
|
||||
# 下载 /photo 整个目录,并使用UI面板展示下载详情
|
||||
aliyunpan d /photo -ui
|
||||
```
|
||||
|
||||
下载的文件默认保存到 **程序所在目录** 的 download/ 目录, 支持设置指定目录, 重名的文件会自动跳过!
|
||||
@@ -323,6 +318,9 @@ aliyunpan upload C:/Users/Administrator/Desktop/1.mp4 C:/Users/Administrator/Des
|
||||
# 将本地的 C:\Users\Administrator\Desktop 整个目录上传到网盘 /视频 目录
|
||||
aliyunpan upload C:/Users/Administrator/Desktop /视频
|
||||
|
||||
# 将本地的 C:\Users\Administrator\Desktop 整个目录上传到网盘 /视频 目录,并使用UI面板展示下载详情
|
||||
aliyunpan upload C:/Users/Administrator/Desktop /视频 -ui
|
||||
|
||||
## 下面演示文件或者文件夹排除功能
|
||||
|
||||
# 将本地的 C:\Users\Administrator\Video 整个目录上传到网盘 /视频 目录,但是排除所有的.jpg文件
|
||||
@@ -345,20 +343,36 @@ aliyunpan upload -exn "\.jpg$" -exn "\.mp3$" C:/Users/Administrator/Video /视
|
||||
```
|
||||
#!/bin/bash
|
||||
|
||||
# 是否开启调试日志
|
||||
# 是否开启调试日志 (1=开启, 0=关闭)
|
||||
export ALIYUNPAN_VERBOSE=0
|
||||
# (可选)配置目录的绝对路径(请更改成你自己的目录)
|
||||
export ALIYUNPAN_CONFIG_DIR=/Users/tickstep/Applications/adrive/config
|
||||
# aliyunpan程序所在的绝对路径(请更改成你自己的目录)
|
||||
export ALIYUNPAN_BIN=/Users/tickstep/Applications/adrive/aliyunpan
|
||||
|
||||
# 本地目录(请更改成你自己的目录)
|
||||
# (可选)配置目录的绝对路径
|
||||
export ALIYUNPAN_CONFIG_DIR="/Users/tickstep/Applications/adrive/config"
|
||||
|
||||
# aliyunpan程序所在的绝对路径
|
||||
export ALIYUNPAN_BIN="/Users/tickstep/Applications/adrive/aliyunpan"
|
||||
|
||||
# 本地目录
|
||||
LOCAL_DIR="/tickstep/Documents/我的文档"
|
||||
# 网盘目录(请更改成你自己的目录)
|
||||
|
||||
# 网盘目录
|
||||
PAN_DIR="/我的文档"
|
||||
|
||||
# 执行上传
|
||||
$ALIYUNPAN_BIN upload -exn "^\." -exn "^@eadir$" "$LOCAL_DIR" $PAN_DIR"
|
||||
# 检查本地目录是否存在
|
||||
if [ ! -d "$LOCAL_DIR" ]; then
|
||||
echo "[ERROR] 本地目录不存在: $LOCAL_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 执行上传(已修复引号、添加安全引用和覆盖策略)
|
||||
"$ALIYUNPAN_BIN" upload \
|
||||
-exn "^\." \
|
||||
-exn "^@eadir$" \
|
||||
-ow \
|
||||
"$LOCAL_DIR" \
|
||||
"$PAN_DIR"
|
||||
|
||||
echo "[INFO] 上传任务执行完毕"
|
||||
```
|
||||
|
||||
增加脚本执行权限
|
||||
@@ -416,25 +430,6 @@ aliyunpan mv <文件/目录1> <文件/目录2> <文件/目录3> ... <目标目
|
||||
aliyunpan mv /我的文档/1.mp4 /
|
||||
```
|
||||
|
||||
## 备份盘和资源库之间转存文件
|
||||
```
|
||||
aliyunpan xcp <文件/目录1> <文件/目录2> <文件/目录3> ... <目标盘目录>
|
||||
```
|
||||
|
||||
注意: 拷贝多个文件和目录时, 请确保每一个文件和目录都存在, 否则拷贝操作会失败
|
||||
|
||||
### 例子
|
||||
```
|
||||
当前程序工作在备份盘下,将备份盘 /1.mp4 文件转存复制到 资源库下的 /来自备份盘/video 目录下
|
||||
aliyunpan xcp /1.mp4 /来自备份盘/video
|
||||
|
||||
当前程序工作在资源库下,将资源库 /1.mp4 文件转存复制到 备份盘下的 /来自资源库/video 目录下
|
||||
aliyunpan xcp /1.mp4 /来自资源库/video
|
||||
|
||||
当前程序工作在备份盘下,将 /我的资源 目录下所有的.mp4文件 复制到 /来自备份盘/video 目录下面,使用通配符匹配
|
||||
aliyunpan xcp /我的资源/*.mp4 /来自备份盘/video
|
||||
```
|
||||
|
||||
## 重命名文件/目录
|
||||
```
|
||||
aliyunpan rename <旧文件/目录名> <新文件/目录名>
|
||||
@@ -544,7 +539,7 @@ aliyunpan sync start -dp 2 -up 1 -dbs 256 -ubs 1024
|
||||
|
||||
### 备份配置文件说明
|
||||
如果你只有一个文件夹进行备份建议直接使用命令行配置启动即可。如果需要同时启动多个备份任务,则可以使用备份配置文件启动同步备份任务。
|
||||
配置文件如下所示,如果你有通过环境变量ALIYUNPAN_CONFIG_DIR设置配置目录,则需要将sync_drive文件夹拷贝到配置的目录中才可以生效。
|
||||
配置文件如下所示,如果你有通过环境变量```ALIYUNPAN_CONFIG_DIR```设置配置目录,则需要将```sync_drive```文件夹拷贝到配置的目录中才可以生效。
|
||||
```
|
||||
配置文件需要保存在:(配置目录)/sync_drive/sync_drive_config.json,样例如下:
|
||||
|
||||
@@ -746,574 +741,10 @@ services:
|
||||
```
|
||||
|
||||
3. sync_handler.js插件说明
|
||||
可以使用JS插件过滤备份的文件。
|
||||
```javascript
|
||||
// ==========================================================================================
|
||||
// aliyunpan JS插件回调处理函数
|
||||
// 支持 JavaScript ECMAScript 5.1 语言规范
|
||||
//
|
||||
// 更多内容请查看官方文档:https://github.com/tickstep/aliyunpan
|
||||
// ==========================================================================================
|
||||
|
||||
// ------------------------------------------------------------------------------------------
|
||||
// 函数说明:同步备份-扫描本地文件前的回调函数
|
||||
//
|
||||
// 参数说明
|
||||
// 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 - 扫描本地文件前参数
|
||||
// {
|
||||
// "localFilePath": "D:\\Program Files\\aliyunpan\\Downloads\\token.bat",
|
||||
// "localFileName": "token.bat",
|
||||
// "localFileSize": 125330,
|
||||
// "localFileType": "file",
|
||||
// "localFileUpdatedAt": "2022-04-14 07:05:12",
|
||||
// "driveId": "19519221"
|
||||
// }
|
||||
// localFilePath - 本地文件绝对完整路径
|
||||
// localFileName - 本地文件名
|
||||
// localFileSize - 本地文件大小,单位B
|
||||
// localFileType - 本地文件类型,file-文件,folder-文件夹
|
||||
// localFileUpdatedAt - 文件修改时间
|
||||
// driveId - 备份的目标网盘ID
|
||||
//
|
||||
// 返回值说明
|
||||
// {
|
||||
// "syncScanLocalApproved": "yes"
|
||||
// }
|
||||
// syncScanLocalApproved - 该文件是否确认扫描,yes-允许扫描,no-禁止扫描。
|
||||
// 禁止扫描的文件不会执行后续的动作,例如上传,下载。
|
||||
// ------------------------------------------------------------------------------------------
|
||||
function syncScanLocalFilePrepareCallback(context, params) {
|
||||
console.log(params);
|
||||
var result = {
|
||||
"syncScanLocalApproved": "yes"
|
||||
};
|
||||
|
||||
// 禁止.开头文件上传
|
||||
if (params["localFileName"].indexOf(".") == 0) {
|
||||
result["syncScanLocalApproved"] = "no";
|
||||
}
|
||||
|
||||
// 禁止~$开头文件上传(office暂存临时文件)
|
||||
if (params["localFileName"].indexOf("~$") == 0) {
|
||||
result["syncScanLocalApproved"] = "no";
|
||||
}
|
||||
|
||||
// 禁止.txt文件上传(正则表达式方式)
|
||||
if (params["localFileName"].search(/.txt$/i) >= 0) {
|
||||
result["syncScanLocalApproved"] = "no";
|
||||
}
|
||||
|
||||
// 禁止password.key文件上传
|
||||
if (params["localFileName"] == "password.key") {
|
||||
result["syncScanLocalApproved"] = "no";
|
||||
}
|
||||
|
||||
// 禁止@eadir文件上传
|
||||
if (params["localFileName"] == "@eadir") {
|
||||
result["syncScanLocalApproved"] = "no";
|
||||
}
|
||||
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 - 扫描云盘文件前参数
|
||||
// {
|
||||
// "driveId": "19519221",
|
||||
// "driveFileName": "token.bat",
|
||||
// "driveFilePath": "/aliyunpan/Downloads/token.bat",
|
||||
// "driveFileSha1": "08FBE28A5B8791A2F50225E2EC5CEEC3C7955A11",
|
||||
// "driveFileSize": 125330,
|
||||
// "driveFileType": "file",
|
||||
// "driveFileUpdatedAt": "2022-04-14 07:05:12"
|
||||
// }
|
||||
// driveId - 网盘ID
|
||||
// driveFileName - 网盘文件名
|
||||
// driveFilePath - 网盘文件绝对完整路径
|
||||
// driveFileSize - 网盘文件大小,单位B
|
||||
// driveFileSha1 - 网盘文件SHA1
|
||||
// driveFileType - 网盘文件类型,file-文件,folder-文件夹
|
||||
// driveFileUpdatedAt - 网盘文件修改时间
|
||||
//
|
||||
// 返回值说明
|
||||
// {
|
||||
// "syncScanPanApproved": "yes"
|
||||
// }
|
||||
// syncScanPanApproved - 该文件是否确认扫描,yes-允许扫描,no-禁止扫描。
|
||||
// 禁止扫描的文件不会执行后续的动作,例如上传,下载。
|
||||
// ------------------------------------------------------------------------------------------
|
||||
function syncScanPanFilePrepareCallback(context, params) {
|
||||
console.log(params);
|
||||
|
||||
var result = {
|
||||
"syncScanPanApproved": "yes"
|
||||
};
|
||||
|
||||
// 禁止.开头文件下载
|
||||
if (params["driveFileName"].indexOf(".") == 0) {
|
||||
result["syncScanPanApproved"] = "no";
|
||||
}
|
||||
|
||||
// 禁止~$开头文件下载(office暂存临时文件)
|
||||
if (params["driveFileName"].indexOf("~$") == 0) {
|
||||
result["syncScanPanApproved"] = "no";
|
||||
}
|
||||
|
||||
// 禁止.txt文件下载(正则表达式方式)
|
||||
// if (params["driveFileName"].search(/.txt$/i) >= 0) {
|
||||
// result["syncScanPanApproved"] = "no";
|
||||
// }
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
可以使用JS插件过滤备份的文件。更多细节请查看文档:[JavaScript插件手册](https://github.com/tickstep/aliyunpan/blob/main/docs/plugin_manual.md#如何使用)
|
||||
|
||||
## JavaScript插件
|
||||
支持javascript插件,你可以按照自己的需要定制上传/下载中关键步骤的行为,最大程度满足自己的个性化需求。
|
||||
例如:
|
||||
1.排除某个特定敏感文件的上传
|
||||
2.上传文件进行改名,但是本地文件不做更改
|
||||
3.上传完成文件,删除本地文件
|
||||
4.上传的文件路径进行更改,但是本地的文件保持不变
|
||||
5.上传文件成功后,通过HTTP通知其他服务
|
||||
6.排除某些网盘文件的下载
|
||||
7.下载的文件进行改名,但是网盘的文件保持不变
|
||||
8.下载的文件路径进行更改,但是网盘的文件保持不变
|
||||
9.下载文件完成后,通过HTTP通知其他服务
|
||||
10.同步备份功能,支持过滤本地文件,或者过滤云盘文件。定制上传或者下载需要同步的文件
|
||||
11.Token刷新失败或者过期,发送外部通知(HTTP&邮件)
|
||||
|
||||
### 如何使用
|
||||
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语言基础,然后按照里面的样例根据自己所需进行改动即可。
|
||||
如果你有通过环境变量ALIYUNPAN_CONFIG_DIR设置配置目录,则需要将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",
|
||||
"Accept": "application/json"
|
||||
};
|
||||
try {
|
||||
var r = PluginUtil.Http.get(header, "https://625f528c53a42eaa07f37e13.mockapi.io/files/1");
|
||||
console.log(r);
|
||||
} catch (e) {
|
||||
if (e !== "Error") {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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",
|
||||
"Accept": "application/json"
|
||||
};
|
||||
try {
|
||||
var reqDataStr = JSON.stringify({
|
||||
"id": "1",
|
||||
"localFilePath": "/usr/local/src/borders_burundi_producer.gram.aab",
|
||||
"localFileSize": 1111,
|
||||
"uploadApproved": false
|
||||
});
|
||||
var r = PluginUtil.Http.post(header, "https://625f528c53a42eaa07f37e13.mockapi.io/files", reqDataStr);
|
||||
console.log(r);
|
||||
} catch (e) {
|
||||
if (e !== "Error") {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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)
|
||||
|
||||
var result = {
|
||||
"uploadApproved": "yes",
|
||||
"driveFilePath": ""
|
||||
};
|
||||
if (params["localFileType"] != "file") {
|
||||
// do nothing
|
||||
return result;
|
||||
}
|
||||
|
||||
// 禁止点号.开头的文件上传
|
||||
if (params["localFileName"].indexOf(".") == 0) {
|
||||
result["uploadApproved"] = "no";
|
||||
}
|
||||
|
||||
// 禁止.txt文件上传
|
||||
if (params["localFileName"].search(/.txt$/i) >= 0) {
|
||||
result["uploadApproved"] = "no";
|
||||
}
|
||||
|
||||
// 禁止password.key文件上传
|
||||
if (params["localFileName"] == "password.key") {
|
||||
result["uploadApproved"] = "no";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.上传文件后删除本地文件
|
||||
使用JavaScript上传插件中的`uploadFileFinishCallback`函数。如下所示:
|
||||
```js
|
||||
function uploadFileFinishCallback(context, params) {
|
||||
console.log(params);
|
||||
if (params["localFileType"] != "file") {
|
||||
// do nothing
|
||||
return;
|
||||
}
|
||||
if (params["uploadResult"] == "success") {
|
||||
PluginUtil.LocalFS.deleteFile(params["localFilePath"]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.下载文件并截断过长的文件名
|
||||
有些文件的路径或者名称太长,下载的时候可能会由于路径名称过程导致无法下载,这时候可以使用JavaScript下载插件中的`downloadFilePrepareCallback`函数定制下载保存的文件名。
|
||||
如下所示:
|
||||
```js
|
||||
function downloadFilePrepareCallback(context, params) {
|
||||
console.log(params)
|
||||
|
||||
var result = {
|
||||
"downloadApproved": "yes",
|
||||
"localFilePath": ""
|
||||
};
|
||||
if (params["driveFileType"] != "file") {
|
||||
return result;
|
||||
}
|
||||
|
||||
// 下面的代码都是分隔路径,方便后面修改路径时使用
|
||||
var filePath = params["localFilePath"];
|
||||
filePath = filePath.replace(/\\/g, "/");
|
||||
|
||||
// 目录完整路径
|
||||
var dirPath = "";
|
||||
// 文件名,不包括后缀名
|
||||
var fileName = "";
|
||||
// 文件后缀名
|
||||
var fileExt = "";
|
||||
|
||||
var idx = filePath.lastIndexOf('/');
|
||||
if (idx > 0) {
|
||||
dirPath = filePath.substring(0,idx);
|
||||
fileName = filePath.substring(idx+1,filePath.length);
|
||||
} else {
|
||||
fileName = filePath;
|
||||
}
|
||||
idx = fileName.lastIndexOf(".")
|
||||
if (idx > 0) {
|
||||
fileExt = fileName.substring(idx,fileName.length);
|
||||
fileName = fileName.substring(0, fileName.length-fileExt.length)
|
||||
}
|
||||
|
||||
// 开始按照需要截断太长的文件路径,例如下面的这个例子:
|
||||
//
|
||||
// dirPath + "/" ==> 这个的意思是保留前面的文件夹路径,如果不需要就去掉
|
||||
// fileName.substr(0,10) ==> 文件名只取前面10个字符,其他的不要了
|
||||
// + fileExt ==> 把文件的后缀名补上
|
||||
var saveFilePath = dirPath + "/" + fileName.substr(0,10) + fileExt;
|
||||
|
||||
// 返回
|
||||
result["localFilePath"] = saveFilePath;
|
||||
return result;
|
||||
}
|
||||
```
|
||||
效果如下:
|
||||
1)假如网盘源路径是:`/亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagram( 《金融时报》和麦肯锡2020年度商业书籍!社交应用如何改变世界?解锁打造估值千亿美元爆品的核心方法! ) by 莎拉·弗莱尔(z-lib.org).mobi`
|
||||
2)下载后保存本地的路径是:`D:\test\亚马逊书籍合集\亚马逊 kindle ebook 大合集5289册\经济金融 (2)\解密Instagra.mobi`
|
||||
```
|
||||
[1] ----
|
||||
文件ID: 630f0623e67c0a1d2e554b1994a29108d089f0d5
|
||||
文件名: 解密Instagram( 《金融时报》和麦肯锡2020年度商业书籍!社交应用如何改变世界?解锁打造估值千亿美元爆品的核心方法! ) by 莎拉·弗莱尔(z-lib.org).mobi
|
||||
文件类型: 文件
|
||||
文件路径: /亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagram( 《金融时报》和麦肯锡2020年度商业书籍!社交应用如何改变世界?解锁打造估值千亿美元爆品的核心方法! ) by 莎拉·弗莱尔(z-lib.org).mobi
|
||||
|
||||
插件修改文件下载保存路径为: D:\test\亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagra.mobi
|
||||
[1] 准备下载: /亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagram( 《金融时报》和麦肯锡2020年度商业书籍!社交应用如何改变世界?解锁打造估值千亿美元爆品的核心方法! ) by 莎拉·弗莱尔(z-lib.org).mobi
|
||||
[1] 将会下载到路径: D:\test\亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagra.mobi
|
||||
[1] 下载开始
|
||||
|
||||
[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.上传文件时去掉指定目录或者文件
|
||||
upload命令本身支持exn参数排除上传文件或目录,但是只能指定文件名称,不能指定文件的路径。如果需要排除指定路径则可以通过插件脚本实现,样例如下:
|
||||
```js
|
||||
function uploadFilePrepareCallback(context, params) {
|
||||
//(自行配置)禁止上传的本地【目录列表】,使用绝对路径,可以配置多个路径用逗号分隔
|
||||
var forbiddenUploadFolders = ["/Users/tickstep/Downloads/up/target","/Users/tickstep/Downloads/up/.idea"]
|
||||
//(自行配置)禁止上传的本地【文件列表】,使用绝对路径,可以配置多个路径用逗号分隔
|
||||
var forbiddenUploadFiles = ["/Users/tickstep/Downloads/up/pom.xml"]
|
||||
|
||||
// -------------------- 以下代码不要修改 --------------------
|
||||
var result = {
|
||||
"uploadApproved": "yes",
|
||||
"driveFilePath": ""
|
||||
};
|
||||
|
||||
// 下面的代码都是分隔路径,方便后面使用
|
||||
var filePath = params["localFilePath"];
|
||||
filePath = filePath.replace(/\\/g, "/");
|
||||
// 目录完整路径
|
||||
var dirPath = "";
|
||||
// 文件名,不包括后缀名
|
||||
var fileName = "";
|
||||
// 文件后缀名
|
||||
var fileExt = "";
|
||||
var idx = filePath.lastIndexOf('/');
|
||||
if (idx > 0) {
|
||||
dirPath = filePath.substring(0,idx);
|
||||
fileName = filePath.substring(idx+1,filePath.length);
|
||||
} else {
|
||||
fileName = filePath;
|
||||
}
|
||||
idx = fileName.lastIndexOf(".")
|
||||
if (idx > 0) {
|
||||
fileExt = fileName.substring(idx,fileName.length);
|
||||
fileName = fileName.substring(0, fileName.length-fileExt.length)
|
||||
}
|
||||
|
||||
if (params["localFileType"] == "file") {
|
||||
for (var i = 0; i < forbiddenUploadFiles.length; i++) {
|
||||
if (forbiddenUploadFiles[i].replace(/\\/g, "/") == params["localFilePath"]) {
|
||||
result["uploadApproved"] = "no"; // 禁止文件上传
|
||||
break
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < forbiddenUploadFolders.length; i++) {
|
||||
if (forbiddenUploadFolders[i].replace(/\\/g, "/") == dirPath) {
|
||||
result["uploadApproved"] = "no"; // 禁止文件上传
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if (params["localFileType"] == "folder") {
|
||||
for (var i = 0; i < forbiddenUploadFolders.length; i++) {
|
||||
if (forbiddenUploadFolders[i].replace(/\\/g, "/") == params["localFilePath"]) {
|
||||
result["uploadApproved"] = "no"; // 禁止文件上传
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
#### 6.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
本程序支持javascript插件,更多细节请查看文档:[JavaScript插件手册](https://github.com/tickstep/aliyunpan/blob/main/docs/plugin_manual.md)
|
||||
|
||||
## 显示和修改程序配置项
|
||||
```
|
||||
@@ -1359,4 +790,4 @@ Windows
|
||||
|
||||
### 第二步
|
||||
打开aliyunpan命令行程序,任何云盘命令都有类似如下日志输出
|
||||

|
||||

|
||||
|
||||
@@ -0,0 +1,553 @@
|
||||
# 目录
|
||||
- [简介](#简介)
|
||||
- [如何使用](#如何使用)
|
||||
- [JS中内置的函数](#JS中内置的函数)
|
||||
+ [console.log()](#consolelog)
|
||||
+ [console.println()](#consoleprintln)
|
||||
+ [PluginUtil.Http.get()](#PluginUtilHttpget)
|
||||
+ [PluginUtil.Http.post()](#PluginUtilHttppost)
|
||||
+ [PluginUtil.LocalFS.deleteFile()](#PluginUtilLocalFSdeleteFile)
|
||||
+ [PluginUtil.PanFS.deleteFile()](#PluginUtilPanFSdeleteFile)
|
||||
+ [PluginUtil.Email.sendTextMail()](#PluginUtilEmailsendTextMail)
|
||||
+ [PluginUtil.Email.sendHtmlMail()](#PluginUtilEmailsendHtmlMail)
|
||||
+ [PluginUtil.KV.putString()](#PluginUtilKVputString)
|
||||
+ [PluginUtil.KV.getString()](#PluginUtilKVgetString)
|
||||
+ [PluginUtil.HashTool.md5Hex()](#PluginUtilHashToolmd5Hex)
|
||||
- [常见场景样例](#常见场景样例)
|
||||
+ [1.禁止特定文件上传](#1禁止特定文件上传)
|
||||
+ [2.上传文件后删除本地文件](#2上传文件后删除本地文件)
|
||||
+ [3.下载文件并截断过长的文件名](#3下载文件并截断过长的文件名)
|
||||
+ [4.上传文件去掉文件名包含的部分字符](#4上传文件去掉文件名包含的部分字符)
|
||||
+ [5.上传文件时过滤指定目录或者文件路径](#5上传文件时过滤指定目录或者文件路径)
|
||||
+ [6.下载云盘文件到本地后删除云盘对应的文件](#6下载云盘文件到本地后删除云盘对应的文件)
|
||||
+ [7.Token刷新失败发送外部通知](#7Token刷新失败发送外部通知)
|
||||
+ [8.每次只下载指定数量的文件](#8每次只下载指定数量的文件)
|
||||
|
||||
# 简介
|
||||
本程序支持javascript插件。通过JS插件,你可以按照自己的需要定制上传、下载、同步、删除过程中关键步骤的行为,最大程度满足自己的个性化需求。
|
||||
例如:
|
||||
1. 排除某个特定敏感文件的上传
|
||||
2. 上传文件进行改名,但是本地文件不做更改
|
||||
3. 上传完成文件,删除本地文件
|
||||
4. 上传的文件路径进行更改,但是本地的文件保持不变
|
||||
5. 上传文件成功后,通过HTTP通知其他服务
|
||||
6. 排除某些网盘文件的下载
|
||||
7. 下载的文件进行改名,但是网盘的文件保持不变
|
||||
8. 下载的文件路径进行更改,但是网盘的文件保持不变
|
||||
9. 下载文件完成后,通过HTTP通知其他服务
|
||||
10. 同步备份功能,支持过滤本地文件,或者过滤云盘文件。定制上传或者下载需要同步的文件
|
||||
|
||||
# 如何使用
|
||||
JS插件的样本文件默认存放在程序所在的```plugin/js```文件夹下,分别为:
|
||||
1. 下载插件(download_handler.js.sample)
|
||||
2. 上传插件(upload_handler.js.sample)
|
||||
3. 删除插件(remove_handler.js.sample)
|
||||
4. 同步备份插件(sync_handler.js.sample)
|
||||
5. 用户Token插件(token_handler.js.sample)
|
||||
|
||||
建议拷贝一份并将后缀名更改为.js,例如:upload_handler.js,不然插件不会生效。
|
||||
你必须具备一定的JS语言基础,然后按照里面的样例根据自己所需进行改动即可。如果你不会JS那也没关系,你可以提issue需求,然后我们开发成员或者网友会给你提供JS脚本代码。
|
||||
注意:如果你有通过环境变量```ALIYUNPAN_CONFIG_DIR```设置配置目录,则需要将plugin文件夹拷贝到配置的目录中才可以生效。
|
||||
|
||||
# JS中内置的函数
|
||||
目前开放了如下函数,你可以在你的js脚本中直接调用,以用于增强JS脚本的扩展性、可玩性以及可适用性。
|
||||
|
||||
## console.log()
|
||||
打印日志,这个日志需要开启debug日志才会在控制台窗口显示
|
||||
```js
|
||||
console.log("hello world");
|
||||
```
|
||||
## console.println()
|
||||
打印日志,这个日志会直接在控制台窗口显示,无需开启debug日志
|
||||
```js
|
||||
console.println("hello world");
|
||||
```
|
||||
|
||||
## 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",
|
||||
"Accept": "application/json"
|
||||
};
|
||||
try {
|
||||
var r = PluginUtil.Http.get(header, "https://625f528c53a42eaa07f37e13.mockapi.io/files/1");
|
||||
console.log(r);
|
||||
} catch (e) {
|
||||
if (e !== "Error") {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 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",
|
||||
"Accept": "application/json"
|
||||
};
|
||||
try {
|
||||
var reqDataStr = JSON.stringify({
|
||||
"id": "1",
|
||||
"localFilePath": "/usr/local/src/borders_burundi_producer.gram.aab",
|
||||
"localFileSize": 1111,
|
||||
"uploadApproved": false
|
||||
});
|
||||
var r = PluginUtil.Http.post(header, "https://625f528c53a42eaa07f37e13.mockapi.io/files", reqDataStr);
|
||||
console.log(r);
|
||||
} catch (e) {
|
||||
if (e !== "Error") {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## PluginUtil.LocalFS.deleteFile()
|
||||
删除本地指定文件,不支持文件夹
|
||||
```
|
||||
PluginUtil.LocalFS.deleteFile(localFilePath);
|
||||
其中:
|
||||
localFilePath - 本地文件的绝对完整路径
|
||||
```
|
||||
样例
|
||||
```js
|
||||
PluginUtil.LocalFS.deleteFile("/Users/tickstep/Downloads/IMG_0884.HEIC");
|
||||
```
|
||||
|
||||
## PluginUtil.PanFS.deleteFile()
|
||||
删除云盘指定文件,支持文件、文件夹
|
||||
```
|
||||
PluginUtil.PanFS.deleteFile(userId, driveId, panFileId);
|
||||
其中:
|
||||
userId - 登录的用户ID
|
||||
driveId - 网盘ID
|
||||
panFileId - 网盘文件ID
|
||||
```
|
||||
样例
|
||||
```js
|
||||
var userId = context["userId"]
|
||||
var driveId = params["driveId"]
|
||||
var driveFileId = params["driveFileId"]
|
||||
if (PluginUtil.PanFS.deleteFile(userId, driveId, driveFileId)) {
|
||||
console.println("插件删除云盘文件成功")
|
||||
}
|
||||
```
|
||||
|
||||
## 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", "文件上传通知", "该文件已经上传完毕");
|
||||
```
|
||||
|
||||
## 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);
|
||||
```
|
||||
|
||||
## PluginUtil.KV.putString()
|
||||
存储键值对,定义如下
|
||||
```
|
||||
PluginUtil.KV.putString(key, value)
|
||||
|
||||
其中:
|
||||
key - 键,字符串
|
||||
value - 值,字符串
|
||||
```
|
||||
样例
|
||||
```js
|
||||
PluginUtil.KV.putString("mykey", "1670419352");
|
||||
```
|
||||
|
||||
## PluginUtil.KV.getString()
|
||||
获取存储的键值,指定对应的键,返回存储的值,如果没有则返回空字符串,定义如下
|
||||
```
|
||||
PluginUtil.KV.getString(key)
|
||||
|
||||
其中:
|
||||
key - 键,字符串
|
||||
```
|
||||
样例
|
||||
```js
|
||||
var value = PluginUtil.KV.getString("mykey");
|
||||
```
|
||||
|
||||
## PluginUtil.HashTool.md5Hex()
|
||||
计算指定字符串text的MD5值,返回hex格式的字符串MD5,定义如下
|
||||
```
|
||||
PluginUtil.HashTool.md5Hex(text)
|
||||
|
||||
其中:
|
||||
text - 字符串
|
||||
```
|
||||
样例
|
||||
```js
|
||||
var md5 = PluginUtil.HashTool.md5Hex("123456");
|
||||
```
|
||||
|
||||
# 常见场景样例
|
||||
这里收集了一些常见的需求样例,可以作为插件定制的样例模板。
|
||||
|
||||
## 1.禁止特定文件上传
|
||||
使用JavaScript上传插件中的`uploadFilePrepareCallback`函数。如下所示:
|
||||
```js
|
||||
function uploadFilePrepareCallback(context, params) {
|
||||
console.log(params)
|
||||
|
||||
var result = {
|
||||
"uploadApproved": "yes",
|
||||
"driveFilePath": ""
|
||||
};
|
||||
if (params["localFileType"] != "file") {
|
||||
// do nothing
|
||||
return result;
|
||||
}
|
||||
|
||||
// 禁止点号.开头的文件上传
|
||||
if (params["localFileName"].indexOf(".") == 0) {
|
||||
result["uploadApproved"] = "no";
|
||||
}
|
||||
|
||||
// 禁止.txt文件上传
|
||||
if (params["localFileName"].search(/.txt$/i) >= 0) {
|
||||
result["uploadApproved"] = "no";
|
||||
}
|
||||
|
||||
// 禁止password.key文件上传
|
||||
if (params["localFileName"] == "password.key") {
|
||||
result["uploadApproved"] = "no";
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
|
||||
## 2.上传文件后删除本地文件
|
||||
使用JavaScript上传插件中的`uploadFileFinishCallback`函数。如下所示:
|
||||
```js
|
||||
function uploadFileFinishCallback(context, params) {
|
||||
console.log(params);
|
||||
if (params["localFileType"] != "file") {
|
||||
// do nothing
|
||||
return;
|
||||
}
|
||||
if (params["uploadResult"] == "success") {
|
||||
PluginUtil.LocalFS.deleteFile(params["localFilePath"]);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 3.下载文件并截断过长的文件名
|
||||
有些文件的路径或者名称太长,下载的时候可能会由于路径名称过程导致无法下载,这时候可以使用JavaScript下载插件中的`downloadFilePrepareCallback`函数定制下载保存的文件名。
|
||||
如下所示:
|
||||
```js
|
||||
function downloadFilePrepareCallback(context, params) {
|
||||
console.log(params)
|
||||
|
||||
var result = {
|
||||
"downloadApproved": "yes",
|
||||
"localFilePath": ""
|
||||
};
|
||||
if (params["driveFileType"] != "file") {
|
||||
return result;
|
||||
}
|
||||
|
||||
// 下面的代码都是分隔路径,方便后面修改路径时使用
|
||||
var filePath = params["localFilePath"];
|
||||
filePath = filePath.replace(/\\/g, "/");
|
||||
|
||||
// 目录完整路径
|
||||
var dirPath = "";
|
||||
// 文件名,不包括后缀名
|
||||
var fileName = "";
|
||||
// 文件后缀名
|
||||
var fileExt = "";
|
||||
|
||||
var idx = filePath.lastIndexOf('/');
|
||||
if (idx > 0) {
|
||||
dirPath = filePath.substring(0,idx);
|
||||
fileName = filePath.substring(idx+1,filePath.length);
|
||||
} else {
|
||||
fileName = filePath;
|
||||
}
|
||||
idx = fileName.lastIndexOf(".")
|
||||
if (idx > 0) {
|
||||
fileExt = fileName.substring(idx,fileName.length);
|
||||
fileName = fileName.substring(0, fileName.length-fileExt.length)
|
||||
}
|
||||
|
||||
// 开始按照需要截断太长的文件路径,例如下面的这个例子:
|
||||
//
|
||||
// dirPath + "/" ==> 这个的意思是保留前面的文件夹路径,如果不需要就去掉
|
||||
// fileName.substr(0,10) ==> 文件名只取前面10个字符,其他的不要了
|
||||
// + fileExt ==> 把文件的后缀名补上
|
||||
var saveFilePath = dirPath + "/" + fileName.substr(0,10) + fileExt;
|
||||
|
||||
// 返回
|
||||
result["localFilePath"] = saveFilePath;
|
||||
return result;
|
||||
}
|
||||
```
|
||||
效果如下:
|
||||
1)假如网盘源路径是:`/亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagram( 《金融时报》和麦肯锡2020年度商业书籍!社交应用如何改变世界?解锁打造估值千亿美元爆品的核心方法! ) by 莎拉·弗莱尔(z-lib.org).mobi`
|
||||
2)下载后保存本地的路径是:`D:\test\亚马逊书籍合集\亚马逊 kindle ebook 大合集5289册\经济金融 (2)\解密Instagra.mobi`
|
||||
```
|
||||
[1] ----
|
||||
文件ID: 630f0623e67c0a1d2e554b1994a29108d089f0d5
|
||||
文件名: 解密Instagram( 《金融时报》和麦肯锡2020年度商业书籍!社交应用如何改变世界?解锁打造估值千亿美元爆品的核心方法! ) by 莎拉·弗莱尔(z-lib.org).mobi
|
||||
文件类型: 文件
|
||||
文件路径: /亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagram( 《金融时报》和麦肯锡2020年度商业书籍!社交应用如何改变世界?解锁打造估值千亿美元爆品的核心方法! ) by 莎拉·弗莱尔(z-lib.org).mobi
|
||||
|
||||
插件修改文件下载保存路径为: D:\test\亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagra.mobi
|
||||
[1] 准备下载: /亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagram( 《金融时报》和麦肯锡2020年度商业书籍!社交应用如何改变世界?解锁打造估值千亿美元爆品的核心方法! ) by 莎拉·弗莱尔(z-lib.org).mobi
|
||||
[1] 将会下载到路径: D:\test\亚马逊书籍合集/亚马逊 kindle ebook 大合集5289册/经济金融 (2)/解密Instagra.mobi
|
||||
[1] 下载开始
|
||||
|
||||
[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.上传文件时过滤指定目录或者文件路径
|
||||
upload命令本身支持exn参数排除上传文件或目录,但是只能指定文件名称,不能指定文件的路径。如果需要排除指定路径则可以通过插件脚本实现,样例如下:
|
||||
```js
|
||||
function uploadFilePrepareCallback(context, params) {
|
||||
//(自行配置)禁止上传的本地【目录列表】,使用绝对路径,可以配置多个路径用逗号分隔
|
||||
var forbiddenUploadFolders = ["/Users/tickstep/Downloads/up/target","/Users/tickstep/Downloads/up/.idea"]
|
||||
//(自行配置)禁止上传的本地【文件列表】,使用绝对路径,可以配置多个路径用逗号分隔
|
||||
var forbiddenUploadFiles = ["/Users/tickstep/Downloads/up/pom.xml"]
|
||||
|
||||
// -------------------- 以下代码不要修改 --------------------
|
||||
var result = {
|
||||
"uploadApproved": "yes",
|
||||
"driveFilePath": ""
|
||||
};
|
||||
|
||||
// 下面的代码都是分隔路径,方便后面使用
|
||||
var filePath = params["localFilePath"];
|
||||
filePath = filePath.replace(/\\/g, "/");
|
||||
// 目录完整路径
|
||||
var dirPath = "";
|
||||
// 文件名,不包括后缀名
|
||||
var fileName = "";
|
||||
// 文件后缀名
|
||||
var fileExt = "";
|
||||
var idx = filePath.lastIndexOf('/');
|
||||
if (idx > 0) {
|
||||
dirPath = filePath.substring(0,idx);
|
||||
fileName = filePath.substring(idx+1,filePath.length);
|
||||
} else {
|
||||
fileName = filePath;
|
||||
}
|
||||
idx = fileName.lastIndexOf(".")
|
||||
if (idx > 0) {
|
||||
fileExt = fileName.substring(idx,fileName.length);
|
||||
fileName = fileName.substring(0, fileName.length-fileExt.length)
|
||||
}
|
||||
|
||||
if (params["localFileType"] == "file") {
|
||||
for (var i = 0; i < forbiddenUploadFiles.length; i++) {
|
||||
if (forbiddenUploadFiles[i].replace(/\\/g, "/") == params["localFilePath"]) {
|
||||
result["uploadApproved"] = "no"; // 禁止文件上传
|
||||
break
|
||||
}
|
||||
}
|
||||
for (var i = 0; i < forbiddenUploadFolders.length; i++) {
|
||||
if (forbiddenUploadFolders[i].replace(/\\/g, "/") == dirPath) {
|
||||
result["uploadApproved"] = "no"; // 禁止文件上传
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if (params["localFileType"] == "folder") {
|
||||
for (var i = 0; i < forbiddenUploadFolders.length; i++) {
|
||||
if (forbiddenUploadFolders[i].replace(/\\/g, "/") == params["localFilePath"]) {
|
||||
result["uploadApproved"] = "no"; // 禁止文件上传
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
```
|
||||
## 6.下载云盘文件到本地后删除云盘对应的文件
|
||||
```js
|
||||
function downloadFileFinishCallback(context, params) {
|
||||
console.log(params)
|
||||
// 云盘文件成功下载到本地后,删除云盘的文件
|
||||
if (params["downloadResult"] == "success") {
|
||||
if (params["driveFileType"] == "file") {
|
||||
// 文件下载成功,删除该云盘文件
|
||||
var userId = context["userId"]
|
||||
var driveId = params["driveId"]
|
||||
var driveFileId = params["driveFileId"]
|
||||
if (PluginUtil.PanFS.deleteFile(userId, driveId, driveFileId)) {
|
||||
console.println("插件删除云盘文件成功:" + params["driveFilePath"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 7.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
## 8.每次只下载指定数量的文件
|
||||
每次运行download下载命令,只下载指定数量的文件,只要数量够了其他文件就跳过不再下载,其他文件等下一次运行download命令的时候再下载。
|
||||
```js
|
||||
function downloadFilePrepareCallback(context, params) {
|
||||
var result = {
|
||||
"downloadApproved": "yes",
|
||||
"localFilePath": ""
|
||||
};
|
||||
|
||||
// 这次下载限制的最大文件总数量
|
||||
const maxCountOfDownloadAction = 3;
|
||||
|
||||
// 只处理文件
|
||||
if (params["driveFileType"] != "file") {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取这次下载动作,已经下载的文件数量
|
||||
var keyOfThisDownloadAction = "download:" + params["downloadActionId"];
|
||||
var valueOfThisDownloadAction = PluginUtil.KV.getString(keyOfThisDownloadAction);
|
||||
if (valueOfThisDownloadAction == "") {
|
||||
valueOfThisDownloadAction = "0";
|
||||
}
|
||||
var countOfThisDownloadAction = parseInt(valueOfThisDownloadAction);
|
||||
if (countOfThisDownloadAction >= maxCountOfDownloadAction) {
|
||||
// 下载数量已达到最大,其他文件不再下载
|
||||
result["downloadApproved"] = "no";
|
||||
return result;
|
||||
}
|
||||
|
||||
// 该文件需要下载,记录相关信息
|
||||
var keyOfThisDownloadFile = "file:" + PluginUtil.HashTool.md5Hex(params["driveFilePath"]);
|
||||
var valueOfThisDownloadFile = PluginUtil.KV.getString(keyOfThisDownloadFile);
|
||||
if (valueOfThisDownloadFile == "finish") {
|
||||
// 已经在下载的文件不做处理
|
||||
return result;
|
||||
}
|
||||
PluginUtil.KV.putString(keyOfThisDownloadFile, "downloading");
|
||||
|
||||
// 更新下载总数量
|
||||
countOfThisDownloadAction += 1;
|
||||
PluginUtil.KV.putString(keyOfThisDownloadAction, String(countOfThisDownloadAction));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function downloadFileFinishCallback(context, params) {
|
||||
var keyOfThisDownloadFile = "file:" + PluginUtil.HashTool.md5Hex(params["driveFilePath"]);
|
||||
console.log(keyOfThisDownloadFile)
|
||||
PluginUtil.KV.putString(keyOfThisDownloadFile, "finish");
|
||||
}
|
||||
```
|
||||
@@ -14,10 +14,11 @@ require (
|
||||
github.com/olekukonko/tablewriter v0.0.2-0.20190618033246-cc27d85e17ce
|
||||
github.com/peterh/liner v1.2.1
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
github.com/tickstep/aliyunpan-api v0.2.6
|
||||
github.com/tickstep/aliyunpan-api v0.2.9
|
||||
github.com/tickstep/bolt v1.3.4
|
||||
github.com/tickstep/library-go v0.1.3
|
||||
github.com/urfave/cli v1.21.1-0.20190817182405-23c83030263f
|
||||
golang.org/x/sys v0.15.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -32,7 +33,6 @@ require (
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/russross/blackfriday v1.5.2 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a // indirect
|
||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
)
|
||||
|
||||
|
||||
@@ -101,8 +101,8 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/tickstep/aliyunpan-api v0.2.6 h1:cjn+bBLcPbquKV80aXv07a7808csWche5ctZ4yLljso=
|
||||
github.com/tickstep/aliyunpan-api v0.2.6/go.mod h1:UvQgQYMIgA5k1QsfKIVnEJ2h/HqDXIkAb16oookNc8o=
|
||||
github.com/tickstep/aliyunpan-api v0.2.9 h1:0n7BNjmKEi/JYqNLhotBnCQJmJE3OAqiDX28q0Gknxs=
|
||||
github.com/tickstep/aliyunpan-api v0.2.9/go.mod h1:UvQgQYMIgA5k1QsfKIVnEJ2h/HqDXIkAb16oookNc8o=
|
||||
github.com/tickstep/bolt v1.3.4 h1:UN1txRsauOfR8a5mUWm5So0ng233PsIMVR5/+3WowCE=
|
||||
github.com/tickstep/bolt v1.3.4/go.mod h1:Y24RwCywbOsKAyHiVcJ4K2QQfED+q67x7PEOH9OG7Q4=
|
||||
github.com/tickstep/library-go v0.1.3 h1:OUj6nkimTsqhHXh5s+PbmHq5hR0m739Y1tu9QpfA2ng=
|
||||
@@ -123,8 +123,9 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359 h1:2B5p2L5IfGiD7+b9BOoRMC6DgObAVZV+Fsp050NqXik=
|
||||
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
|
||||
@@ -132,6 +132,7 @@ func CmdAlbum() cli.Command {
|
||||
IsOverwrite: c.Bool("ow"),
|
||||
SaveTo: saveTo,
|
||||
Parallel: 0,
|
||||
SliceParallel: 0,
|
||||
Load: 0,
|
||||
MaxRetry: pandownload.DefaultDownloadMaxRetry,
|
||||
NoCheck: false,
|
||||
@@ -258,8 +259,14 @@ func RunShareAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
||||
options.Parallel = config.MaxFileDownloadParallelNum
|
||||
}
|
||||
// 设置单个文件下载分片线程数
|
||||
if options.SliceParallel < 1 {
|
||||
options.SliceParallel = 1
|
||||
if options.SliceParallel < 1 { // 用户没有主动设置,则使用下面自动配置的策略
|
||||
if options.Parallel > 1 {
|
||||
// 如果是多文件下载,则一个文件只能同时使用1个线程
|
||||
options.SliceParallel = 1
|
||||
} else {
|
||||
// 如果是单文件下载,则可以使用3个最大线程
|
||||
options.SliceParallel = 3
|
||||
}
|
||||
}
|
||||
|
||||
// 保存文件的本地根文件夹
|
||||
@@ -392,7 +399,6 @@ func RunShareAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
||||
Cfg: &newCfg, // 复制一份新的cfg
|
||||
PanClient: panClient,
|
||||
VerbosePrinter: panCommandVerbose,
|
||||
PrintFormat: downloadPrintFormat(options.Load),
|
||||
ParentTaskExecutor: &executor,
|
||||
DownloadStatistic: statistic,
|
||||
IsPrintStatus: options.IsPrintStatus,
|
||||
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/file/downloader"
|
||||
"github.com/tickstep/aliyunpan/internal/functions/pandownload"
|
||||
"github.com/tickstep/aliyunpan/internal/functions/panlogin"
|
||||
"github.com/tickstep/aliyunpan/internal/taskframework"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/aliyunpan/library/requester/transfer"
|
||||
@@ -628,7 +629,8 @@ func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
||||
go func(flag *int32) {
|
||||
for atomic.LoadInt32(flag) == 0 {
|
||||
time.Sleep(time.Duration(1) * time.Minute)
|
||||
if RefreshWebTokenInNeed(activeUser, config.Config.DeviceName) {
|
||||
re := RefreshWebTokenInNeed(activeUser)
|
||||
if re.Code == panlogin.SUCCESS {
|
||||
logger.Verboseln("update access token for download task")
|
||||
}
|
||||
}
|
||||
@@ -733,7 +735,6 @@ func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
||||
Cfg: &newCfg, // 复制一份新的cfg
|
||||
PanClient: panClient,
|
||||
VerbosePrinter: panCommandVerbose,
|
||||
PrintFormat: downloadPrintFormat(options.Load),
|
||||
ParentTaskExecutor: &executor,
|
||||
DownloadStatistic: statistic,
|
||||
IsPrintStatus: options.IsPrintStatus,
|
||||
|
||||
@@ -15,6 +15,12 @@ package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
@@ -23,27 +29,26 @@ import (
|
||||
"github.com/tickstep/aliyunpan/internal/global"
|
||||
"github.com/tickstep/aliyunpan/internal/log"
|
||||
"github.com/tickstep/aliyunpan/internal/taskframework"
|
||||
"github.com/tickstep/aliyunpan/internal/ui"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/aliyunpan/library/requester/transfer"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/library-go/requester/rio/speeds"
|
||||
"github.com/urfave/cli"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
type (
|
||||
//DownloadOptions 下载可选参数
|
||||
DownloadOptions struct {
|
||||
// DownloadActionId 下载动作ID,唯一标识一次下载动作的ID。这个ID每次下载任务启动的时候会自动生成,同一次下载任务下载的文件,这个ID都是同一个值。
|
||||
DownloadActionId string
|
||||
IsPrintStatus bool
|
||||
IsExecutedPermission bool
|
||||
IsOverwrite bool
|
||||
SaveTo string
|
||||
Parallel int // 文件下载最大线程数
|
||||
SliceParallel int // 单个文件分片下载最大线程数
|
||||
Parallel int // 文件下载最大线程数,即同时进行下载文件的数量,阿里云盘限制最大为3
|
||||
SliceParallel int // 单个文件分片下载最大线程数,阿里云盘限制最大为3
|
||||
Load int
|
||||
MaxRetry int
|
||||
NoCheck bool
|
||||
@@ -51,6 +56,7 @@ type (
|
||||
DriveId string
|
||||
ExcludeNames []string // 排除的文件名,包括文件夹和文件。即这些文件/文件夹不进行下载,支持正则表达式
|
||||
IsMultiUserDownload bool // 是否启用多用户联合下载
|
||||
IsUseUIDashboard bool // 是否使用UI下载面板显示下载进度
|
||||
}
|
||||
|
||||
// LocateDownloadOption 获取下载链接可选参数
|
||||
@@ -131,12 +137,13 @@ func CmdDownload() cli.Command {
|
||||
}
|
||||
|
||||
do := &DownloadOptions{
|
||||
DownloadActionId: utils.UuidStr(),
|
||||
IsPrintStatus: c.Bool("status"),
|
||||
IsExecutedPermission: c.Bool("x"),
|
||||
IsOverwrite: c.Bool("ow"),
|
||||
SaveTo: saveTo,
|
||||
Parallel: c.Int("p"),
|
||||
SliceParallel: 3,
|
||||
SliceParallel: c.Int("sp"),
|
||||
Load: 0,
|
||||
MaxRetry: c.Int("retry"),
|
||||
NoCheck: c.Bool("nocheck"),
|
||||
@@ -144,22 +151,10 @@ func CmdDownload() cli.Command {
|
||||
DriveId: parseDriveId(c),
|
||||
ExcludeNames: c.StringSlice("exn"),
|
||||
IsMultiUserDownload: c.Bool("md"),
|
||||
IsUseUIDashboard: c.Bool("ui"),
|
||||
}
|
||||
|
||||
// 获取下载文件锁,保证下载操作单实例
|
||||
//locker := filelocker.NewFileLocker(config.GetLockerDir() + "/aliyunpan-download")
|
||||
//if e := filelocker.LockFile(locker, 0755, true, 5*time.Second); e != nil {
|
||||
// logger.Verboseln(e)
|
||||
// fmt.Println("本应用其他实例正在执行下载,请先停止或者等待其完成")
|
||||
// return nil
|
||||
//}
|
||||
|
||||
RunDownload(c.Args(), do)
|
||||
|
||||
// 释放文件锁
|
||||
//if locker != nil {
|
||||
// filelocker.UnlockFile(locker)
|
||||
//}
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
@@ -188,11 +183,11 @@ func CmdDownload() cli.Command {
|
||||
Usage: "parallel,指定同时进行下载文件的数量(取值范围:1 ~ 3)",
|
||||
Value: 1,
|
||||
},
|
||||
//cli.IntFlag{
|
||||
// Name: "sp",
|
||||
// Usage: "slice parallel,指定单个文件下载的最大线程(分片)数(取值范围:1 ~ 3)",
|
||||
// Value: 1,
|
||||
//},
|
||||
cli.IntFlag{
|
||||
Name: "sp",
|
||||
Usage: "slice parallel,指定单个文件下载的最大线程(分片)数(取值范围:1 ~ 3)",
|
||||
Value: 0,
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "retry",
|
||||
Usage: "下载失败最大重试次数",
|
||||
@@ -220,17 +215,14 @@ func CmdDownload() cli.Command {
|
||||
Name: "md",
|
||||
Usage: "(BETA) Multi-User Download,使用多用户联合下载,可以对单一文件叠加所有登录用户的下载速度",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "ui",
|
||||
Usage: "(BETA) 使用UI面板显示下载详情和进度,更加直观和友好",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func downloadPrintFormat(load int) string {
|
||||
if load <= 1 {
|
||||
return pandownload.DefaultPrintFormat
|
||||
}
|
||||
return "\r[%s] ↓ %s/%s %s/s in %s, left %s ..."
|
||||
}
|
||||
|
||||
// RunDownload 执行下载网盘内文件
|
||||
func RunDownload(paths []string, options *DownloadOptions) {
|
||||
activeUser := GetActiveUser()
|
||||
@@ -292,8 +284,14 @@ func RunDownload(paths []string, options *DownloadOptions) {
|
||||
}
|
||||
|
||||
// 设置单个文件下载分片线程数
|
||||
if options.SliceParallel < 1 {
|
||||
options.SliceParallel = 1
|
||||
if options.SliceParallel < 1 { // 用户没有主动设置,则使用下面自动配置的策略
|
||||
if options.Parallel > 1 {
|
||||
// 如果是多文件下载,则一个文件只能同时使用1个线程
|
||||
options.SliceParallel = 1
|
||||
} else {
|
||||
// 如果是单文件下载,则可以使用3个最大线程
|
||||
options.SliceParallel = 3
|
||||
}
|
||||
}
|
||||
|
||||
// 保存文件的本地根文件夹
|
||||
@@ -358,7 +356,6 @@ func RunDownload(paths []string, options *DownloadOptions) {
|
||||
// 阿里OpenAPI规定:文件分片下载的并发数为3,即某用户使用 App 时,可以同时下载 1 个文件的 3 个分片,或者同时下载 3 个文件的各 1 个分片。
|
||||
options.SliceParallel = userCount * 3
|
||||
}
|
||||
fmt.Printf("\n[0] 当前文件下载最大并发量为: %d, 单文件下载分片线程数为: %d, 下载缓存为: %s\n", options.Parallel, options.SliceParallel, converter.ConvertFileSize(int64(cfg.CacheSize), 2))
|
||||
|
||||
var (
|
||||
panClient = activeUser.PanClient()
|
||||
@@ -378,38 +375,68 @@ func RunDownload(paths []string, options *DownloadOptions) {
|
||||
// 全局速度统计
|
||||
globalSpeedsStat := &speeds.Speeds{}
|
||||
|
||||
// 下载统计UI面板
|
||||
var dashboard *ui.DashboardPanel = nil
|
||||
if options.IsUseUIDashboard && options.ShowProgress && !options.IsPrintStatus && ui.IsTerminal(os.Stdout) {
|
||||
dashboard = ui.NewDashboardPanel(ui.DashboardPanelDownload, cfg.MaxParallel, globalSpeedsStat, &ui.DashboardOptions{
|
||||
Title: "下载统计UI面板",
|
||||
ActiveSlots: 3, // 下载文件进度展示,最大同时展示3个
|
||||
MaxHistory: 50, // 下载日志显示,最多同时显示50条,这个会按照窗口大小进行自适应显示
|
||||
})
|
||||
}
|
||||
logf := func(format string, a ...interface{}) {
|
||||
if dashboard != nil {
|
||||
dashboard.Logf(format, a...)
|
||||
return
|
||||
}
|
||||
fmt.Printf(format, a...)
|
||||
}
|
||||
|
||||
// 下载记录器
|
||||
fileRecorder := log.NewFileRecorder(config.GetLogDir() + "/download_file_records.csv")
|
||||
|
||||
// 输出当前下载配置信息
|
||||
logf("\n[0] 当前文件下载最大并发量为: %d, 单文件下载分片线程数为: %d, 下载缓存为: %s\n", options.Parallel, options.SliceParallel, converter.ConvertFileSize(int64(cfg.CacheSize), 2))
|
||||
|
||||
// 阿里云盘下载并发超额警告,阿里云盘下载最大并发数为3
|
||||
if options.Parallel*options.SliceParallel > 3 {
|
||||
logf("[0] 警告:当前下载并发配置数已超过阿里云盘限制的最大并发数,下载任务会被风控容易导致下载失败\n")
|
||||
}
|
||||
|
||||
// 处理队列
|
||||
for k := range paths {
|
||||
// 使用通配符匹配
|
||||
fileList, err2 := matchPathByShellPattern(options.DriveId, paths[k])
|
||||
if err2 != nil {
|
||||
fmt.Printf("获取文件出错,请稍后重试: %s\n", paths[k])
|
||||
logf("获取文件出错,请稍后重试: %s\n", paths[k])
|
||||
continue
|
||||
}
|
||||
if fileList == nil || len(fileList) == 0 {
|
||||
// 文件不存在
|
||||
fmt.Printf("文件不存在: %s\n", paths[k])
|
||||
logf("文件不存在: %s\n", paths[k])
|
||||
continue
|
||||
}
|
||||
// 排序,按名称排序,从小到大
|
||||
sort.Slice(fileList, func(i, j int) bool {
|
||||
return fileList[i].FileName < fileList[j].FileName
|
||||
})
|
||||
// 逐一下载
|
||||
for _, f := range fileList {
|
||||
newCfg := *cfg
|
||||
|
||||
// 是否排除下载
|
||||
if utils.IsExcludeFile(f.Path, &newCfg.ExcludeNames) {
|
||||
fmt.Printf("排除文件: %s\n", f.Path)
|
||||
logf("排除文件: %s\n", f.Path)
|
||||
continue
|
||||
}
|
||||
|
||||
// 匹配的文件
|
||||
unit := pandownload.DownloadTaskUnit{
|
||||
DownloadActionId: options.DownloadActionId,
|
||||
Cfg: &newCfg, // 复制一份新的cfg
|
||||
PanClient: panClient,
|
||||
SubPanClientList: subPanClientList,
|
||||
VerbosePrinter: panCommandVerbose,
|
||||
PrintFormat: downloadPrintFormat(options.Load),
|
||||
ParentTaskExecutor: &executor,
|
||||
DownloadStatistic: statistic,
|
||||
IsPrintStatus: options.IsPrintStatus,
|
||||
@@ -421,6 +448,7 @@ func RunDownload(paths []string, options *DownloadOptions) {
|
||||
DriveId: options.DriveId,
|
||||
GlobalSpeedsStat: globalSpeedsStat,
|
||||
FileRecorder: fileRecorder,
|
||||
UI: dashboard,
|
||||
}
|
||||
|
||||
// 设置储存的路径
|
||||
@@ -433,16 +461,30 @@ func RunDownload(paths []string, options *DownloadOptions) {
|
||||
unit.SavePath = GetActiveUser().GetSavePath(f.Path)
|
||||
}
|
||||
info := executor.Append(&unit, options.MaxRetry)
|
||||
fmt.Printf("[%s] 加入下载队列: %s\n", info.Id(), f.Path)
|
||||
if dashboard != nil {
|
||||
dashboard.RegisterTask(info.Id(), f.Path, f.FileSize, f.IsFile())
|
||||
}
|
||||
logf("[%s] 加入下载队列: %s\n", info.Id(), f.Path)
|
||||
}
|
||||
}
|
||||
|
||||
// 开始计时
|
||||
statistic.StartTimer()
|
||||
|
||||
// 启动UI面板显示
|
||||
if dashboard != nil {
|
||||
dashboard.Start()
|
||||
}
|
||||
|
||||
// 开始执行
|
||||
executor.Execute()
|
||||
|
||||
// 关闭UI面板
|
||||
if dashboard != nil {
|
||||
dashboard.Close()
|
||||
}
|
||||
|
||||
// 完成下载,输出统计结果
|
||||
fmt.Printf("\n下载结束, 时间: %s, 数据总量: %s\n", utils.ConvertTime(statistic.Elapsed()), converter.ConvertFileSize(statistic.TotalSize(), 2))
|
||||
|
||||
// 输出失败的文件列表
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"fmt"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
@@ -69,6 +70,7 @@ func CmdLs() cli.Command {
|
||||
`,
|
||||
Category: "阿里云盘",
|
||||
Before: ReloadConfigFunc,
|
||||
After: SaveConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
@@ -144,7 +146,11 @@ func RunLs(driveId, targetPath string, lsOptions *LsOptions,
|
||||
// 获取目标路径文件信息
|
||||
targetPathInfo, err := activeUser.PanClient().OpenapiPanClient().FileInfoByPath(driveId, targetPath)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
if err.Code == apierror.ApiCodeFileNotFoundCode {
|
||||
fmt.Println("指定目录不存在: " + targetPath)
|
||||
} else {
|
||||
fmt.Println(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
+68
-2
@@ -19,10 +19,12 @@ import (
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/plugins"
|
||||
"github.com/urfave/cli"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CmdRm() cli.Command {
|
||||
@@ -72,9 +74,26 @@ func CmdRm() cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func queryFileListByPath(activeUser *config.PanUser, driveId, filePathPattern string) (files []*aliyunpan.FileEntity, e error) {
|
||||
if !strings.ContainsAny(filePathPattern, aliyunpan.ShellPatternCharacters) {
|
||||
// 没有通配符,直接查询
|
||||
fi, err := activeUser.PanClient().OpenapiPanClient().FileInfoByPath(driveId, filePathPattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
files = append(files, fi)
|
||||
} else {
|
||||
// 包含通配符,使用通配符查询
|
||||
files, e = matchPathByShellPattern(driveId, filePathPattern)
|
||||
}
|
||||
return files, e
|
||||
}
|
||||
|
||||
// RunRemove 执行 批量删除文件/目录
|
||||
func RunRemove(driveId string, paths ...string) {
|
||||
activeUser := GetActiveUser()
|
||||
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
|
||||
plugin, _ := pluginManger.GetPlugin()
|
||||
|
||||
cacheCleanDirs := []string{}
|
||||
failedRmPaths := make([]string, 0, len(paths))
|
||||
@@ -82,17 +101,62 @@ func RunRemove(driveId string, paths ...string) {
|
||||
|
||||
for _, p := range paths {
|
||||
absolutePath := path.Clean(activeUser.PathJoin(driveId, p))
|
||||
fileList, err1 := matchPathByShellPattern(driveId, absolutePath)
|
||||
|
||||
// 查询路径对应的文件信息
|
||||
fileList, err1 := queryFileListByPath(activeUser, driveId, absolutePath)
|
||||
if err1 != nil {
|
||||
failedRmPaths = append(failedRmPaths, absolutePath)
|
||||
continue
|
||||
}
|
||||
if fileList == nil || len(fileList) == 0 {
|
||||
// 失败,文件不存在
|
||||
// 跳过,没有满足条件的文件
|
||||
failedRmPaths = append(failedRmPaths, absolutePath)
|
||||
continue
|
||||
}
|
||||
// 调用插件
|
||||
approvedToRemoveFiles := []*aliyunpan.FileEntity{}
|
||||
pluginParam := &plugins.RemoveFilePrepareParams{
|
||||
Count: len(fileList),
|
||||
Items: make([]*plugins.RemoveFilePrepareItem, 0),
|
||||
}
|
||||
for _, f := range fileList {
|
||||
pluginParam.Items = append(pluginParam.Items, &plugins.RemoveFilePrepareItem{
|
||||
DriveId: f.DriveId,
|
||||
DriveFileId: f.FileId,
|
||||
DriveFileName: f.FileName,
|
||||
DriveFilePath: f.Path,
|
||||
DriveFileSize: f.FileSize,
|
||||
DriveFileType: f.FileType,
|
||||
DriveFileUpdatedAt: f.UpdatedAt,
|
||||
DriveFileCreatedAt: f.CreatedAt,
|
||||
})
|
||||
}
|
||||
if removeFilePrepareResult, er := plugin.RemoveFilePrepareCallback(plugins.GetContext(config.Config.ActiveUser()), pluginParam); er == nil && removeFilePrepareResult != nil {
|
||||
for _, f := range fileList {
|
||||
matchResult := false
|
||||
for _, r := range removeFilePrepareResult.Result {
|
||||
if strings.Compare(f.FileId, r.DriveFileId) == 0 {
|
||||
matchResult = true
|
||||
if strings.Compare("yes", r.RemoveApproved) != 0 {
|
||||
// skip this file
|
||||
fmt.Printf("插件不允许删除该文件: %s\n", f.Path)
|
||||
} else {
|
||||
approvedToRemoveFiles = append(approvedToRemoveFiles, f)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if !matchResult {
|
||||
// 该文件没有确认结果,则默认删除
|
||||
approvedToRemoveFiles = append(approvedToRemoveFiles, f)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 默认删除全部文件
|
||||
approvedToRemoveFiles = fileList
|
||||
}
|
||||
|
||||
for _, f := range approvedToRemoveFiles {
|
||||
// 删除匹配的文件
|
||||
fdr, err := activeUser.PanClient().OpenapiPanClient().FileDelete(&aliyunpan.FileBatchActionParam{
|
||||
DriveId: driveId,
|
||||
@@ -127,5 +191,7 @@ func RunRemove(driveId string, paths ...string) {
|
||||
fmt.Println("操作成功, 以下文件/目录已删除, 可在云盘文件回收站找回: ")
|
||||
pnt()
|
||||
activeUser.DeleteCache(cacheCleanDirs)
|
||||
} else {
|
||||
fmt.Println("本次操作没有删除任何文件")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/urfave/cli"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -196,7 +197,7 @@ func RunOpenShareSet(modeFlag, driveId string, paths []string, expiredTime strin
|
||||
}
|
||||
|
||||
fmt.Printf("创建快传链接成功\n")
|
||||
fmt.Printf("链接:%s\n", r.ShareUrl)
|
||||
fmt.Printf("链接:%s\n", strings.ReplaceAll(r.ShareUrl, "https://www.aliyundrive.com", "https://www.alipan.com"))
|
||||
} else {
|
||||
// 分享
|
||||
r, err1 := panClient.OpenapiPanClient().ShareLinkCreate(aliyunpan.ShareCreateParam{
|
||||
@@ -219,10 +220,11 @@ func RunOpenShareSet(modeFlag, driveId string, paths []string, expiredTime strin
|
||||
} else if modeFlag == "2" {
|
||||
fmt.Printf("创建公开分享链接成功\n")
|
||||
}
|
||||
shareUrl := strings.ReplaceAll(r.ShareUrl, "https://www.aliyundrive.com", "https://www.alipan.com")
|
||||
if len(sharePwd) > 0 {
|
||||
fmt.Printf("链接:%s 提取码:%s\n", r.ShareUrl, r.SharePwd)
|
||||
fmt.Printf("链接:%s 提取码:%s\n", shareUrl, r.SharePwd)
|
||||
} else {
|
||||
fmt.Printf("链接:%s\n", r.ShareUrl)
|
||||
fmt.Printf("链接:%s\n", shareUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,6 +398,7 @@ func RunSync(defaultTask *syncdrive.SyncTask, cycleMode syncdrive.CycleMode, fil
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
syncMgr.DoTaskSyncCompletelyPluginCallback()
|
||||
}
|
||||
} else {
|
||||
if cycleMode == syncdrive.CycleInfiniteLoop {
|
||||
@@ -423,6 +424,7 @@ func RunSync(defaultTask *syncdrive.SyncTask, cycleMode syncdrive.CycleMode, fil
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
syncMgr.DoTaskSyncCompletelyPluginCallback()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+76
-36
@@ -15,12 +15,6 @@ package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/internal/log"
|
||||
"github.com/tickstep/aliyunpan/internal/plugins"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/library-go/requester/rio/speeds"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
@@ -28,6 +22,14 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/internal/log"
|
||||
"github.com/tickstep/aliyunpan/internal/plugins"
|
||||
"github.com/tickstep/aliyunpan/internal/ui"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/library-go/requester/rio/speeds"
|
||||
|
||||
"github.com/tickstep/library-go/logger"
|
||||
|
||||
"github.com/urfave/cli"
|
||||
@@ -54,17 +56,18 @@ const (
|
||||
type (
|
||||
// UploadOptions 上传可选项
|
||||
UploadOptions struct {
|
||||
AllParallel int // 所有文件并发上传数量,即可以同时并发上传多少个文件
|
||||
Parallel int // 单个文件并发上传数量
|
||||
MaxRetry int
|
||||
MaxTimeoutSec int // http请求超时时间,单位秒
|
||||
NoRapidUpload bool
|
||||
ShowProgress bool
|
||||
IsOverwrite bool // 覆盖已存在的文件,如果同名文件已存在则移到回收站里
|
||||
IsSkipSameName bool // 跳过已存在的文件,即使文件内容不一致(不检查SHA1)
|
||||
DriveId string
|
||||
ExcludeNames []string // 排除的文件名,包括文件夹和文件。即这些文件/文件夹不进行上传,支持正则表达式
|
||||
BlockSize int64 // 分片大小
|
||||
AllParallel int // 所有文件并发上传数量,即可以同时并发上传多少个文件
|
||||
Parallel int // 单个文件并发上传数量
|
||||
MaxRetry int
|
||||
MaxTimeoutSec int // http请求超时时间,单位秒
|
||||
NoRapidUpload bool
|
||||
ShowProgress bool
|
||||
IsOverwrite bool // 覆盖已存在的文件,如果同名文件已存在则移到回收站里
|
||||
IsSkipSameName bool // 跳过已存在的文件,即使文件内容不一致(不检查SHA1)
|
||||
DriveId string
|
||||
ExcludeNames []string // 排除的文件名,包括文件夹和文件。即这些文件/文件夹不进行上传,支持正则表达式
|
||||
BlockSize int64 // 分片大小
|
||||
IsUseUIDashboard bool // 是否使用UI面板显示上传进度
|
||||
}
|
||||
)
|
||||
|
||||
@@ -114,6 +117,10 @@ var UploadFlags = []cli.Flag{
|
||||
Usage: "block size,上传分片大小,单位KB。推荐值:1024 ~ 10240。当上传极大单文件时候请适当调高该值",
|
||||
Value: 10240,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "ui",
|
||||
Usage: "(BETA) 使用UI面板显示上传详情和进度,更加直观和友好",
|
||||
},
|
||||
}
|
||||
|
||||
func CmdUpload() cli.Command {
|
||||
@@ -192,17 +199,18 @@ func CmdUpload() cli.Command {
|
||||
//}
|
||||
|
||||
RunUpload(subArgs[:c.NArg()-1], subArgs[c.NArg()-1], &UploadOptions{
|
||||
AllParallel: c.Int("p"), // 多文件上传的时候,允许同时并行上传的文件数量
|
||||
Parallel: 1, // 一个文件同时多少个线程并发上传的数量。阿里云盘只支持单线程按顺序进行文件part数据上传,所以只能是1
|
||||
MaxRetry: c.Int("retry"),
|
||||
MaxTimeoutSec: timeout,
|
||||
NoRapidUpload: c.Bool("norapid"),
|
||||
ShowProgress: !c.Bool("np"),
|
||||
IsOverwrite: c.Bool("ow"),
|
||||
IsSkipSameName: c.Bool("skip"),
|
||||
DriveId: parseDriveId(c),
|
||||
ExcludeNames: c.StringSlice("exn"),
|
||||
BlockSize: int64(c.Int("bs") * 1024),
|
||||
AllParallel: c.Int("p"), // 多文件上传的时候,允许同时并行上传的文件数量
|
||||
Parallel: 1, // 一个文件同时多少个线程并发上传的数量。阿里云盘只支持单线程按顺序进行文件part数据上传,所以只能是1
|
||||
MaxRetry: c.Int("retry"),
|
||||
MaxTimeoutSec: timeout,
|
||||
NoRapidUpload: c.Bool("norapid"),
|
||||
ShowProgress: !c.Bool("np"),
|
||||
IsOverwrite: c.Bool("ow"),
|
||||
IsSkipSameName: c.Bool("skip"),
|
||||
DriveId: parseDriveId(c),
|
||||
ExcludeNames: c.StringSlice("exn"),
|
||||
BlockSize: int64(c.Int("bs") * 1024),
|
||||
IsUseUIDashboard: c.Bool("ui"),
|
||||
})
|
||||
|
||||
// 释放文件锁
|
||||
@@ -290,6 +298,23 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
|
||||
// 全局速度统计
|
||||
globalSpeedsStat := &speeds.Speeds{}
|
||||
|
||||
// 上传统计UI面板
|
||||
var dashboard *ui.DashboardPanel = nil
|
||||
if opt.IsUseUIDashboard && opt.ShowProgress && ui.IsTerminal(os.Stdout) {
|
||||
dashboard = ui.NewDashboardPanel(ui.DashboardPanelUpload, opt.AllParallel, globalSpeedsStat, &ui.DashboardOptions{
|
||||
Title: "上传统计UI面板",
|
||||
ActiveSlots: 3, // 下载文件进度展示,最大同时展示3个
|
||||
MaxHistory: 50, // 下载日志显示,最多同时显示50条,这个会按照窗口大小进行自适应显示
|
||||
})
|
||||
}
|
||||
logf := func(format string, a ...interface{}) {
|
||||
if dashboard != nil {
|
||||
dashboard.Logf(format, a...)
|
||||
return
|
||||
}
|
||||
fmt.Printf(format, a...)
|
||||
}
|
||||
|
||||
// 获取当前插件
|
||||
plugin, _ := pluginManger.GetPlugin()
|
||||
|
||||
@@ -304,7 +329,7 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
|
||||
|
||||
// 是否排除上传
|
||||
if utils.IsExcludeFile(curPath, &opt.ExcludeNames) {
|
||||
fmt.Printf("排除文件: %s\n", curPath)
|
||||
logf("排除文件: %s\n", curPath)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -326,7 +351,7 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
|
||||
|
||||
// 是否排除上传
|
||||
if utils.IsExcludeFile(file.LogicPath, &opt.ExcludeNames) {
|
||||
fmt.Printf("排除文件: %s\n", file.LogicPath)
|
||||
logf("排除文件: %s\n", file.LogicPath)
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
@@ -355,13 +380,13 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
|
||||
if uploadFilePrepareResult, er := plugin.UploadFilePrepareCallback(plugins.GetContext(activeUser), pluginParam); er == nil && uploadFilePrepareResult != nil {
|
||||
if strings.Compare("yes", uploadFilePrepareResult.UploadApproved) != 0 {
|
||||
// skip upload this file
|
||||
fmt.Printf("插件禁止该文件上传: %s\n", file.LogicPath)
|
||||
logf("插件禁止该文件上传: %s\n", file.LogicPath)
|
||||
return filepath.SkipDir
|
||||
}
|
||||
if uploadFilePrepareResult.DriveFilePath != "" {
|
||||
targetSavePanRelativePath := strings.TrimPrefix(uploadFilePrepareResult.DriveFilePath, "/")
|
||||
subSavePath = path.Clean(savePath + aliyunpan.PathSeparator + targetSavePanRelativePath)
|
||||
fmt.Printf("插件修改文件网盘保存路径为: %s\n", subSavePath)
|
||||
logf("插件修改文件网盘保存路径为: %s\n", subSavePath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -384,22 +409,26 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
|
||||
IsSkipSameName: opt.IsSkipSameName,
|
||||
GlobalSpeedsStat: globalSpeedsStat,
|
||||
FileRecorder: fileRecorder,
|
||||
UI: dashboard,
|
||||
}, opt.MaxRetry)
|
||||
fmt.Printf("[%s] 加入上传队列: %s\n", taskinfo.Id(), file.LogicPath)
|
||||
logf("[%s] 加入上传队列: %s\n", taskinfo.Id(), file.LogicPath)
|
||||
if dashboard != nil {
|
||||
dashboard.RegisterTask(taskinfo.Id(), file.LogicPath, fi.Size(), !fi.IsDir())
|
||||
}
|
||||
} else {
|
||||
// 创建文件夹
|
||||
// 这样空文件夹也可以正确上传
|
||||
saveFilePath := subSavePath
|
||||
if saveFilePath != "/" {
|
||||
folderCreateMutex.Lock()
|
||||
fmt.Printf("正在检测和创建云盘文件夹: %s\n", saveFilePath)
|
||||
logf("正在检测和创建云盘文件夹: %s\n", saveFilePath)
|
||||
_, apierr1 := activeUser.PanClient().OpenapiPanClient().FileInfoByPath(opt.DriveId, saveFilePath)
|
||||
time.Sleep(1 * time.Second)
|
||||
if apierr1 != nil && apierr1.Code == apierror.ApiCodeFileNotFoundCode {
|
||||
logger.Verbosef("%s 创建云盘文件夹: %s\n", utils.NowTimeStr(), saveFilePath)
|
||||
rs, apierr := activeUser.PanClient().OpenapiPanClient().MkdirByFullPath(opt.DriveId, saveFilePath)
|
||||
if apierr != nil || rs.FileId == "" {
|
||||
fmt.Printf("创建云盘文件夹失败: %s\n", saveFilePath)
|
||||
logf("创建云盘文件夹失败: %s\n", saveFilePath)
|
||||
}
|
||||
}
|
||||
folderCreateMutex.Unlock()
|
||||
@@ -411,19 +440,30 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
|
||||
file := localfile.NewSymlinkFile(curPath)
|
||||
if err = localfile.WalkAllFile(file, walkFunc); err != nil {
|
||||
if err != filepath.SkipDir {
|
||||
fmt.Printf("警告: 遍历错误: %s\n", err)
|
||||
logf("警告: 遍历错误: %s\n", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 执行上传任务
|
||||
var failedList []*lane.Deque
|
||||
|
||||
// 启动UI面板显示
|
||||
if dashboard != nil {
|
||||
dashboard.Start()
|
||||
}
|
||||
|
||||
executor.Execute()
|
||||
failed := executor.FailedDeque()
|
||||
if failed.Size() > 0 {
|
||||
failedList = append(failedList, failed)
|
||||
}
|
||||
|
||||
// 关闭UI面板
|
||||
if dashboard != nil {
|
||||
dashboard.Close()
|
||||
}
|
||||
|
||||
fmt.Printf("\n")
|
||||
fmt.Printf("上传结束, 时间: %s, 数据总量: %s\n", utils.ConvertTime(statistic.Elapsed()), converter.ConvertFileSize(statistic.TotalSize(), 2))
|
||||
|
||||
|
||||
+128
-22
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/urfave/cli"
|
||||
"math"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"path"
|
||||
@@ -142,9 +143,9 @@ func NewWebLoginToken(accessToken string, expired int64) aliyunpan_web.WebLoginT
|
||||
}
|
||||
|
||||
// RefreshWebTokenInNeed 刷新 webapi access token
|
||||
func RefreshWebTokenInNeed(activeUser *config.PanUser, deviceName string) bool {
|
||||
func RefreshWebTokenInNeed(activeUser *config.PanUser) *panlogin.CommonResultError {
|
||||
if activeUser == nil {
|
||||
return false
|
||||
return panlogin.NewCommonResultError("activeUser is nil")
|
||||
}
|
||||
|
||||
// refresh expired web token
|
||||
@@ -153,7 +154,7 @@ func RefreshWebTokenInNeed(activeUser *config.PanUser, deviceName string) bool {
|
||||
cz := time.FixedZone("CST", 8*3600) // 东8区
|
||||
expiredTime := time.Unix(activeUser.WebapiToken.Expired, 0).In(cz)
|
||||
now := time.Now()
|
||||
if (expiredTime.Unix() - now.Unix()) <= (2 * 60) { // 有效期小于2min就刷新
|
||||
if (expiredTime.Unix() - now.Unix()) <= (10 * 60) { // 有效期小于10min就刷新
|
||||
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
|
||||
plugin, _ := pluginManger.GetPlugin()
|
||||
params := &plugins.UserTokenRefreshFinishParams{
|
||||
@@ -167,9 +168,12 @@ func RefreshWebTokenInNeed(activeUser *config.PanUser, deviceName string) bool {
|
||||
// need update refresh token
|
||||
logger.Verboseln("web access token expired, get new from server")
|
||||
loginHelper := panlogin.NewLoginHelper(config.DefaultTokenServiceWebHost)
|
||||
wt, e := loginHelper.GetWebapiNewToken(activeUser.TicketId, activeUser.UserId, activeUser.PanClient().WebapiPanClient().GetAccessToken())
|
||||
if e != nil {
|
||||
logger.Verboseln("get web token from server error: ", e)
|
||||
wt, re := loginHelper.GetWebapiNewToken(activeUser.TicketId, activeUser.UserId, activeUser.PanClient().WebapiPanClient().GetAccessToken())
|
||||
if re != nil {
|
||||
logger.Verboseln("get web token from server error: ", re.Msg)
|
||||
if re.Code != panlogin.SUCCESS {
|
||||
return re
|
||||
}
|
||||
}
|
||||
if wt != nil {
|
||||
params.Result = "success"
|
||||
@@ -191,42 +195,51 @@ func RefreshWebTokenInNeed(activeUser *config.PanUser, deviceName string) bool {
|
||||
}
|
||||
|
||||
// create new signature
|
||||
_, e := activeUser.PanClient().WebapiPanClient().CreateSession(nil)
|
||||
if e != nil {
|
||||
logger.Verboseln("call CreateSession error in RefreshWebTokenInNeed: " + e.Error())
|
||||
_, e1 := activeUser.PanClient().WebapiPanClient().CreateSession(nil)
|
||||
if e1 != nil {
|
||||
logger.Verboseln("call CreateSession error in RefreshWebTokenInNeed: " + e1.Error())
|
||||
}
|
||||
return &panlogin.CommonResultError{
|
||||
Code: 0,
|
||||
Msg: "",
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
// token refresh error
|
||||
// if token has expired, callback plugin api for notify
|
||||
if now.Unix() >= expiredTime.Unix() {
|
||||
params.Result = "fail"
|
||||
params.Message = e.Error()
|
||||
params.Message = re.Msg
|
||||
params.OldToken = activeUser.WebapiToken.AccessToken
|
||||
if er1 := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(activeUser), params); er1 != nil {
|
||||
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er1.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return &panlogin.CommonResultError{
|
||||
Code: 0,
|
||||
Msg: "web token is validate, not need to refresh",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
return panlogin.NewCommonResultError("activeUser is invalidate")
|
||||
}
|
||||
|
||||
// RefreshOpenTokenInNeed 刷新 openapi access token
|
||||
func RefreshOpenTokenInNeed(activeUser *config.PanUser) bool {
|
||||
func RefreshOpenTokenInNeed(activeUser *config.PanUser) *panlogin.CommonResultError {
|
||||
if activeUser == nil {
|
||||
return false
|
||||
return panlogin.NewCommonResultError("activeUser is nil")
|
||||
}
|
||||
|
||||
// OpenAPI的token只有2个小时有效期,需要在token过期之前进行刷新并把获取到的新token存储在本地
|
||||
// refresh expired openapi token
|
||||
if activeUser.PanClient().OpenapiPanClient() != nil {
|
||||
if activeUser.OpenapiToken != nil && len(activeUser.OpenapiToken.AccessToken) > 0 {
|
||||
cz := time.FixedZone("CST", 8*3600) // 东8区
|
||||
expiredTime := time.Unix(activeUser.OpenapiToken.Expired, 0).In(cz)
|
||||
now := time.Now().In(cz)
|
||||
if (expiredTime.Unix() - now.Unix()) <= (2 * 60) { // 有效期小于2min就刷新
|
||||
if (expiredTime.Unix() - now.Unix()) <= (10 * 60) { // 有效期小于10min就刷新
|
||||
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
|
||||
plugin, _ := pluginManger.GetPlugin()
|
||||
params := &plugins.UserTokenRefreshFinishParams{
|
||||
@@ -240,11 +253,15 @@ func RefreshOpenTokenInNeed(activeUser *config.PanUser) bool {
|
||||
// need update refresh token
|
||||
logger.Verboseln("openapi access token expired, get new from server")
|
||||
loginHelper := panlogin.NewLoginHelper(config.DefaultTokenServiceWebHost)
|
||||
wt, e := loginHelper.GetOpenapiNewToken(activeUser.TicketId, activeUser.UserId, activeUser.PanClient().OpenapiPanClient().GetAccessToken())
|
||||
if e != nil {
|
||||
logger.Verboseln("get openapi token from server error: ", e)
|
||||
wt, re := loginHelper.GetOpenapiNewToken(activeUser.TicketId, activeUser.UserId, activeUser.PanClient().OpenapiPanClient().GetAccessToken())
|
||||
if re != nil {
|
||||
logger.Verboseln("get openapi token from server error: ", re.Msg)
|
||||
if re.Code != panlogin.SUCCESS {
|
||||
return re
|
||||
}
|
||||
}
|
||||
if wt != nil {
|
||||
// 存储新token到本地
|
||||
params.Result = "success"
|
||||
params.OldToken = activeUser.OpenapiToken.AccessToken
|
||||
params.NewToken = wt.AccessToken
|
||||
@@ -261,23 +278,112 @@ func RefreshOpenTokenInNeed(activeUser *config.PanUser) bool {
|
||||
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er1.Error())
|
||||
}
|
||||
|
||||
return true
|
||||
return &panlogin.CommonResultError{
|
||||
Code: 0,
|
||||
Msg: "",
|
||||
}
|
||||
} else {
|
||||
// token refresh error
|
||||
// if token has expired, callback plugin api for notify
|
||||
if now.Unix() >= expiredTime.Unix() {
|
||||
params.Result = "fail"
|
||||
params.Message = e.Error()
|
||||
params.OldToken = activeUser.WebapiToken.AccessToken
|
||||
params.Message = re.Msg
|
||||
params.OldToken = activeUser.OpenapiToken.AccessToken
|
||||
if er1 := plugin.UserTokenRefreshFinishCallback(plugins.GetContext(activeUser), params); er1 != nil {
|
||||
logger.Verbosef("UserTokenRefreshFinishCallback error: " + er1.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return &panlogin.CommonResultError{
|
||||
Code: 0,
|
||||
Msg: "openapi token is validate, not need to refresh",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
return panlogin.NewCommonResultError("activeUser is invalidate")
|
||||
}
|
||||
|
||||
// AutomaticallyRefreshTokenTask 自动刷新Token的后台任务
|
||||
func AutomaticallyRefreshTokenTask() {
|
||||
go func() {
|
||||
// 延迟启动自动刷新Token任务,因为主程序启动的时候默认会进行Token的初始化和刷新,避免无效的并行初始化Token
|
||||
time.Sleep(time.Duration(15) * time.Minute)
|
||||
|
||||
// 检测延迟时间(分钟)
|
||||
delayMinuteTime := time.Duration(1) * time.Minute
|
||||
//delayMinuteTime := time.Duration(10) * time.Second // for test
|
||||
// 请求重试次数
|
||||
attempt := 1
|
||||
for {
|
||||
// Token刷新进程,不管是CLI命令行模式,还是直接命令模式,本刷新任务都会执行
|
||||
time.Sleep(delayMinuteTime) // 定时器,每1分钟调用一次
|
||||
|
||||
// 重新加载配置文件
|
||||
ReloadConfigFunc(nil)
|
||||
activeUser := config.Config.ActiveUser()
|
||||
if activeUser != nil && activeUser.UserId != "" && activeUser.PanClient() != nil {
|
||||
// 刷新Openapi端Token
|
||||
errResult := RefreshOpenTokenInNeed(activeUser)
|
||||
attempt += 1
|
||||
if errResult != nil && errResult.Code == panlogin.SUCCESS {
|
||||
// 刷新成功,重置定时器
|
||||
attempt = 1
|
||||
delayMinuteTime = time.Duration(1) * time.Minute
|
||||
|
||||
// 刷新Webapi端Token
|
||||
RefreshWebTokenInNeed(activeUser)
|
||||
|
||||
// 保存新token到配置文件
|
||||
SaveConfigFunc(nil)
|
||||
} else {
|
||||
// 刷新Token失败
|
||||
logger.Verboseln("刷新Token失败:", errResult.Msg)
|
||||
if errResult.Code == panlogin.ERROR_NEED_LOGIN_AGAIN {
|
||||
// token已经失效,需要重新登录
|
||||
logger.Verboseln("token已经失效,需要重新登录,退出自动刷新Token任务")
|
||||
return
|
||||
} else if errResult.Code == panlogin.ERROR {
|
||||
// 访问错误
|
||||
delayMinuteTime = ProgressiveBackoffAlg(attempt)
|
||||
} else if errResult.Code == panlogin.ERROR_TOO_MANY_REQUESTS {
|
||||
// 请求过于频繁
|
||||
delayMinuteTime = ProgressiveBackoffAlg(attempt)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// ProgressiveBackoffAlg 指数退降算法
|
||||
func ProgressiveBackoffAlg(attempt int) time.Duration {
|
||||
maxRetries := 120
|
||||
baseDelay := float64(1)
|
||||
maxDelay := float64(1440)
|
||||
|
||||
if attempt >= maxRetries {
|
||||
return time.Duration(maxDelay) * time.Minute
|
||||
}
|
||||
|
||||
// 计算当前延迟
|
||||
var delay float64
|
||||
if attempt < 100 {
|
||||
// 前 100 次:线性增长 + 小指数基数(缓慢增加)
|
||||
delay = baseDelay * (1 + float64(attempt)/5) // 调整分母控制增长速度
|
||||
} else {
|
||||
// 100 次后:标准指数退避
|
||||
delay = baseDelay * math.Pow(1.5, float64(attempt-100))
|
||||
if delay > maxDelay {
|
||||
delay = maxDelay
|
||||
}
|
||||
}
|
||||
|
||||
// 添加随机抖动(最多 20% 的扰动)
|
||||
jitterSec := rand.Float64() * delay * 0.2 // ±20% 随机扰动
|
||||
delay += jitterSec
|
||||
return time.Duration(delay * float64(time.Minute))
|
||||
}
|
||||
|
||||
func isIncludeFile(pattern string, fileName string) bool {
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestProgressiveBackoffAlg(t *testing.T) {
|
||||
for attempt := 1; attempt <= 150; attempt++ {
|
||||
delayTime := ProgressiveBackoffAlg(attempt)
|
||||
fmt.Printf("第%d次,延迟分钟: %.2f\n", attempt, delayTime.Minutes())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// 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_local
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan/internal/command"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/urfave/cli"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// CmdLocalCd 切换本地工作目录
|
||||
func CmdLocalCd() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "lcd",
|
||||
Category: "本地命令",
|
||||
Usage: "切换本地工作目录",
|
||||
Description: `
|
||||
aliyunpan lcd <本地目录, 绝对路径或相对路径>
|
||||
|
||||
示例:
|
||||
|
||||
切换 /我的资源 工作目录:
|
||||
aliyunpan lcd /我的资源
|
||||
|
||||
切换上级目录:
|
||||
aliyunpan lcd ..
|
||||
|
||||
切换根目录:
|
||||
aliyunpan lcd /
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() == 0 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
RunChangeLocalDirectory(c.Args().Get(0))
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
}
|
||||
}
|
||||
|
||||
// CmdLocalPwd 当前本地工作目录
|
||||
func CmdLocalPwd() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "lpwd",
|
||||
Usage: "输出本地工作目录",
|
||||
Category: "本地命令",
|
||||
Before: command.ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
lwd := config.Config.LocalWorkdir
|
||||
if lwd == "" {
|
||||
// 默认为用户主页目录
|
||||
lwd = GetLocalHomeDir()
|
||||
config.Config.LocalWorkdir = lwd
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
lwd = strings.ReplaceAll(lwd, "/", "\\")
|
||||
} else {
|
||||
// Unix-like system, so just assume Unix
|
||||
lwd = strings.ReplaceAll(lwd, "\\", "/")
|
||||
}
|
||||
fmt.Println("本地工作目录: " + lwd)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RunChangeLocalDirectory(targetPath string) {
|
||||
targetPath = LocalPathJoin(targetPath)
|
||||
|
||||
// 获取目标路径文件信息
|
||||
localFileInfo, er := os.Stat(targetPath)
|
||||
if er != nil {
|
||||
fmt.Println("目录路径不存在")
|
||||
return
|
||||
}
|
||||
if !localFileInfo.IsDir() {
|
||||
fmt.Printf("错误: %s 不是一个目录 (文件夹)\n", targetPath)
|
||||
return
|
||||
}
|
||||
config.Config.LocalWorkdir = strings.ReplaceAll(targetPath, "\\", "/")
|
||||
fmt.Printf("改变本地工作目录: %s\n", config.Config.LocalWorkdir)
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
// 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_local
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/urfave/cli"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CmdLocalLs() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "lls",
|
||||
Category: "本地命令",
|
||||
Usage: "列出本地目录",
|
||||
Description: `
|
||||
列出当前本地工作目录内的文件和目录, 或指定目录内的文件和目录
|
||||
|
||||
示例:
|
||||
|
||||
列出 我的资源 内的文件和目录
|
||||
aliyunpan lls 我的资源
|
||||
|
||||
绝对路径
|
||||
aliyunpan lls /我的资源
|
||||
|
||||
详细列出 我的资源 内的文件和目录
|
||||
aliyunpan lls /我的资源
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
var (
|
||||
orderBy aliyunpan.FileOrderBy = aliyunpan.FileOrderByUpdatedAt
|
||||
orderSort aliyunpan.FileOrderDirection = aliyunpan.FileOrderDirectionDesc
|
||||
showAllFile bool = false
|
||||
)
|
||||
if c.IsSet("a") {
|
||||
showAllFile = true
|
||||
}
|
||||
|
||||
switch {
|
||||
case c.IsSet("asc"):
|
||||
orderSort = aliyunpan.FileOrderDirectionAsc
|
||||
case c.IsSet("desc"):
|
||||
orderSort = aliyunpan.FileOrderDirectionDesc
|
||||
default:
|
||||
orderSort = aliyunpan.FileOrderDirectionDesc
|
||||
}
|
||||
|
||||
switch {
|
||||
case c.IsSet("time"):
|
||||
orderBy = aliyunpan.FileOrderByUpdatedAt
|
||||
case c.IsSet("name"):
|
||||
orderBy = aliyunpan.FileOrderByName
|
||||
case c.IsSet("size"):
|
||||
orderBy = aliyunpan.FileOrderBySize
|
||||
default:
|
||||
orderBy = aliyunpan.FileOrderByUpdatedAt
|
||||
}
|
||||
|
||||
RunLocalLs(c.Args().Get(0), showAllFile, orderBy, orderSort)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "a",
|
||||
Usage: "显示所有文件",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "asc",
|
||||
Usage: "升序排序",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "desc",
|
||||
Usage: "降序排序",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "time",
|
||||
Usage: "根据修改时间排序",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "name",
|
||||
Usage: "根据文件名排序",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "size",
|
||||
Usage: "根据大小排序",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RunLocalLs(targetPath string,
|
||||
showAllFile bool,
|
||||
orderBy aliyunpan.FileOrderBy, orderDirection aliyunpan.FileOrderDirection) {
|
||||
targetPath = LocalPathJoin(targetPath)
|
||||
|
||||
// 获取目标路径文件信息
|
||||
localFileInfo, er := os.Stat(targetPath)
|
||||
if er != nil {
|
||||
fmt.Println("目录路径不存在")
|
||||
return
|
||||
}
|
||||
if !localFileInfo.IsDir() {
|
||||
fmt.Println("指定的路径不是目录")
|
||||
return
|
||||
}
|
||||
|
||||
fileEntryList, err := os.ReadDir(targetPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
fileInfoList := []os.FileInfo{}
|
||||
folderInfoList := []os.FileInfo{}
|
||||
for _, entry := range fileEntryList {
|
||||
if fi, e := entry.Info(); e == nil {
|
||||
if strings.HasPrefix(fi.Name(), ".") {
|
||||
if !showAllFile {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if fi.IsDir() {
|
||||
folderInfoList = append(folderInfoList, fi)
|
||||
} else {
|
||||
fileInfoList = append(fileInfoList, fi)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 过滤
|
||||
|
||||
// 排序
|
||||
sortFileInfoList(folderInfoList, orderBy, orderDirection)
|
||||
sortFileInfoList(fileInfoList, orderBy, orderDirection)
|
||||
|
||||
// 合并并渲染列表
|
||||
renderFileList := append(folderInfoList, fileInfoList...)
|
||||
renderTable(targetPath, renderFileList)
|
||||
}
|
||||
|
||||
func sortFileInfoList(fileInfoList []os.FileInfo,
|
||||
orderBy aliyunpan.FileOrderBy, orderDirection aliyunpan.FileOrderDirection) []os.FileInfo {
|
||||
switch orderBy {
|
||||
case aliyunpan.FileOrderByUpdatedAt: // 基于修改时间排序
|
||||
sort.Slice(fileInfoList, func(i, j int) bool {
|
||||
if orderDirection == aliyunpan.FileOrderDirectionAsc {
|
||||
return fileInfoList[i].ModTime().UnixMilli() < fileInfoList[j].ModTime().UnixMilli()
|
||||
} else {
|
||||
return fileInfoList[i].ModTime().UnixMilli() > fileInfoList[j].ModTime().UnixMilli()
|
||||
}
|
||||
})
|
||||
break
|
||||
case aliyunpan.FileOrderByName: // 基于文件名排序
|
||||
sort.Slice(fileInfoList, func(i, j int) bool {
|
||||
if orderDirection == aliyunpan.FileOrderDirectionAsc {
|
||||
return fileInfoList[i].Name() < fileInfoList[j].Name()
|
||||
} else {
|
||||
return fileInfoList[i].Name() > fileInfoList[j].Name()
|
||||
}
|
||||
})
|
||||
break
|
||||
case aliyunpan.FileOrderBySize: // 基于文件大小排序
|
||||
sort.Slice(fileInfoList, func(i, j int) bool {
|
||||
if orderDirection == aliyunpan.FileOrderDirectionAsc {
|
||||
return fileInfoList[i].Size() < fileInfoList[j].Size()
|
||||
} else {
|
||||
return fileInfoList[i].Size() > fileInfoList[j].Size()
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
return fileInfoList
|
||||
}
|
||||
|
||||
func renderTable(path string, files []os.FileInfo) {
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
var (
|
||||
fileCount, dirCount, totalSize int64
|
||||
)
|
||||
|
||||
tb.SetHeader([]string{"#", "文件大小", "修改日期", "文件(目录)"})
|
||||
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_RIGHT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT})
|
||||
for k, file := range files {
|
||||
if file.IsDir() {
|
||||
dirCount += 1
|
||||
tb.Append([]string{strconv.Itoa(k + 1), "-", file.ModTime().Format("2006-01-02 15:04:05"), file.Name() + aliyunpan.PathSeparator})
|
||||
continue
|
||||
}
|
||||
fileCount += 1
|
||||
totalSize += file.Size()
|
||||
tb.Append([]string{strconv.Itoa(k + 1), converter.ConvertFileSize(file.Size(), 2), file.ModTime().Format("2006-01-02 15:04:05"), file.Name()})
|
||||
}
|
||||
tb.Append([]string{"", "总: " + converter.ConvertFileSize(totalSize, 2), "", fmt.Sprintf("文件总数: %d, 目录总数: %d", fileCount, dirCount)})
|
||||
|
||||
fmt.Printf("\n本地当前目录: %s\n", path)
|
||||
fmt.Printf("----\n")
|
||||
tb.Render()
|
||||
fmt.Printf("----\n")
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package command_local
|
||||
|
||||
import (
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/library/homedir"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetLocalHomeDir 获取本地用户主目录
|
||||
func GetLocalHomeDir() string {
|
||||
// 默认为用户主页目录
|
||||
if hd, e := os.UserHomeDir(); e == nil {
|
||||
return hd
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// LocalPathJoin 拼接本地路径
|
||||
func LocalPathJoin(p string) string {
|
||||
p = path.Clean(strings.ReplaceAll(p, "\\", "/"))
|
||||
if filepath.IsAbs(p) {
|
||||
return p
|
||||
} else if strings.HasPrefix(p, "~") {
|
||||
if d, e := homedir.Expand(p); e == nil {
|
||||
return d
|
||||
}
|
||||
}
|
||||
wd := config.Config.LocalWorkdir
|
||||
if wd == "" {
|
||||
wd = GetLocalHomeDir()
|
||||
}
|
||||
return path.Join(wd, p)
|
||||
}
|
||||
|
||||
// LocalPathDir 获取当前文件夹的父文件夹路径
|
||||
func LocalPathDir(p string) string {
|
||||
p = strings.ReplaceAll(p, "\\", "/")
|
||||
if strings.HasSuffix(p, ":/") { // windows卷标路径,例如:C:\ 、D:\、E:\
|
||||
return p
|
||||
}
|
||||
parentDirPath := path.Dir(LocalPathClean(p))
|
||||
return parentDirPath
|
||||
}
|
||||
|
||||
// LocalPathBase 获取当前路径的文件(文件夹)名
|
||||
func LocalPathBase(p string) string {
|
||||
p = strings.ReplaceAll(p, "\\", "/")
|
||||
if strings.HasSuffix(p, ":/") { // windows卷标路径,例如:C:\ 、D:\、E:\
|
||||
return ""
|
||||
}
|
||||
baseName := path.Base(LocalPathClean(p))
|
||||
return baseName
|
||||
}
|
||||
|
||||
// LocalPathClean 规范化本地文件夹路径
|
||||
func LocalPathClean(p string) string {
|
||||
cleanPath := path.Clean(strings.ReplaceAll(p, "\\", "/"))
|
||||
return cleanPath
|
||||
}
|
||||
@@ -54,7 +54,7 @@ const (
|
||||
DefaultFileDownloadParallelNum = 5
|
||||
|
||||
// MaxFileDownloadParallelNum 最大文件下载并发数量。过大会被阿里云盘风控,导致无法下载
|
||||
MaxFileDownloadParallelNum = 20
|
||||
MaxFileDownloadParallelNum = 3
|
||||
|
||||
// DefaultTokenServiceWebHost 默认的token服务
|
||||
DefaultTokenServiceWebHost = "https://api.tickstep.com"
|
||||
@@ -115,6 +115,9 @@ type PanConfig struct {
|
||||
ClientId string `json:"clientId"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
|
||||
// 本地工作目录(lcd/lpwd/lls命令使用)
|
||||
LocalWorkdir string `json:"localWorkdir"`
|
||||
|
||||
configFilePath string
|
||||
configFile *os.File
|
||||
fileMu sync.Mutex
|
||||
|
||||
@@ -109,7 +109,7 @@ func (c *PanConfig) PrintTable() {
|
||||
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT})
|
||||
tb.AppendBulk([][]string{
|
||||
[]string{"cache_size", converter.ConvertFileSize(int64(c.CacheSize), 2), "1KB ~ 256KB", "下载缓存, 如果硬盘占用高或下载速度慢, 请尝试调大此值"},
|
||||
[]string{"max_download_parallel", strconv.Itoa(c.MaxDownloadParallel), "1 ~ 20", "最大下载并发量,即同时下载文件最大数量"},
|
||||
[]string{"max_download_parallel", strconv.Itoa(c.MaxDownloadParallel), "1 ~ 3", "最大下载并发量,即同时下载文件最大数量"},
|
||||
[]string{"max_upload_parallel", strconv.Itoa(c.MaxUploadParallel), "1 ~ 20", "最大上传并发量,即同时上传文件最大数量"},
|
||||
[]string{"max_download_rate", showMaxRate(c.MaxDownloadRate), "", "限制单个文件最大下载速度, 0代表不限制"},
|
||||
[]string{"max_upload_rate", showMaxRate(c.MaxUploadRate), "", "限制单个文件最大上传速度, 0代表不限制"},
|
||||
|
||||
@@ -152,16 +152,23 @@ doOpenLoginAct:
|
||||
AccessToken: openapiToken.AccessToken,
|
||||
ExpiredAt: openapiToken.Expired,
|
||||
}, nil)
|
||||
openPanClient.SetAccessTokenRefreshCallback(func(userId string, newToken openapi.ApiToken) error {
|
||||
// api层自动刷新Token,回调通知程序
|
||||
logger.Verboseln("openapi token refresh, use new token")
|
||||
openapiToken.AccessToken = newToken.AccessToken
|
||||
openapiToken.Expired = newToken.ExpiredAt
|
||||
return nil
|
||||
})
|
||||
|
||||
// open api token maybe expired
|
||||
// check & refresh new one
|
||||
openUserInfo, err := openPanClient.GetUserInfo()
|
||||
if err != nil {
|
||||
if err.Code == apierror.ApiCodeTokenExpiredCode && tryRefreshOpenToken {
|
||||
if err.Code == apierror.ApiCodeTokenExpiredCode && tryRefreshOpenToken { // 如果是token过期,则尝试刷新
|
||||
tryRefreshOpenToken = false
|
||||
wt, e := loginHelper.GetOpenapiNewToken(ticketId, userId, openapiToken.AccessToken)
|
||||
if e != nil {
|
||||
logger.Verboseln("get openapi token from server error: ", e)
|
||||
wt, re := loginHelper.GetOpenapiNewToken(ticketId, userId, openapiToken.AccessToken)
|
||||
if re != nil {
|
||||
logger.Verboseln("get openapi token from server error: ", re.Msg)
|
||||
return nil, apierror.NewFailedApiError("get new openapi token error, try login again")
|
||||
}
|
||||
if wt != nil {
|
||||
@@ -169,7 +176,11 @@ doOpenLoginAct:
|
||||
AccessToken: wt.AccessToken,
|
||||
Expired: wt.Expired,
|
||||
}
|
||||
|
||||
// TODO: 检查token有效期
|
||||
}
|
||||
|
||||
// 回到token检测流程,必须确保token是有效
|
||||
time.Sleep(time.Duration(1) * time.Second)
|
||||
goto doOpenLoginAct
|
||||
}
|
||||
@@ -207,7 +218,7 @@ doWebLoginAct:
|
||||
tryRefreshWebToken = false
|
||||
wt, e := loginHelper.GetWebapiNewToken(ticketId, userId, webapiToken.AccessToken)
|
||||
if e != nil {
|
||||
logger.Verboseln("get web token from server error: ", e)
|
||||
logger.Verboseln("get web token from server error: ", e.Msg)
|
||||
}
|
||||
if wt != nil {
|
||||
webapiToken = &PanClientToken{
|
||||
@@ -246,6 +257,7 @@ doWebLoginAct:
|
||||
WorkdirFileEntity: *aliyunpan.NewFileEntityForRootDir(),
|
||||
}
|
||||
u.PanClient().OpenapiPanClient().SetAccessTokenRefreshCallback(func(userId string, newToken openapi.ApiToken) error {
|
||||
// api层自动刷新Token,回调通知程序
|
||||
logger.Verboseln("openapi token refresh, update for user")
|
||||
u.OpenapiToken = &PanClientToken{
|
||||
AccessToken: newToken.AccessToken,
|
||||
|
||||
@@ -46,6 +46,16 @@ func (pl *PanUserList) String() string {
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
// GetUserByUserId 根据用户ID获取用户信息
|
||||
func (pl *PanUserList) GetUserByUserId(userId string) *PanUser {
|
||||
for _, u := range *pl {
|
||||
if u.UserId == userId {
|
||||
return u
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AverageParallel 返回平均的下载最大并发量
|
||||
func AverageParallel(parallel, downloadLoad int) int {
|
||||
if downloadLoad < 1 {
|
||||
|
||||
@@ -346,6 +346,7 @@ func (mt *Monitor) ResetWorker(worker *Worker) {
|
||||
// 忽略, 返回
|
||||
return
|
||||
case StatusCodeDownloadUrlExpired: // 下载链接已经过期
|
||||
logger.Verbosef("download url expired, reset worker: %d\n", worker.ID())
|
||||
worker.RefreshDownloadUrl()
|
||||
break
|
||||
case StatusCodeDownloadUrlExceedMaxConcurrency: // 下载遇到限流报错
|
||||
|
||||
@@ -15,8 +15,6 @@ package downloader
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/library-go/requester"
|
||||
mathrand "math/rand"
|
||||
"mime"
|
||||
"net/url"
|
||||
@@ -24,6 +22,10 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/library-go/requester"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -102,15 +104,15 @@ func fixCacheSize(size *int) {
|
||||
|
||||
// IsUrlExpired 下载链接是否已过期。过期返回True
|
||||
func IsUrlExpired(urlStr string) bool {
|
||||
u, err := url.Parse(urlStr)
|
||||
expiredTimeSec, err := utils.GetExpiredTimeSecFromOSSUrl(urlStr)
|
||||
if err != nil {
|
||||
// 解析错误,默认过期
|
||||
return true
|
||||
}
|
||||
expiredTimeSecStr := u.Query().Get("x-oss-expires")
|
||||
expiredTimeSec, _ := strconv.ParseInt(expiredTimeSecStr, 10, 64)
|
||||
if (expiredTimeSec - time.Now().Unix()) <= 5 { // 小于5秒钟
|
||||
// expired
|
||||
return true
|
||||
}
|
||||
// 返回未过期
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -205,6 +205,7 @@ func (wer *Worker) Cancel() error {
|
||||
|
||||
// Reset 重设连接
|
||||
func (wer *Worker) Reset() {
|
||||
logger.Verbosef("Reset worker: %d\n", wer.ID())
|
||||
if wer.resetFunc == nil {
|
||||
logger.Verbosef("DEBUG: worker: resetFunc not set")
|
||||
return
|
||||
@@ -220,13 +221,15 @@ func (wer *Worker) Reset() {
|
||||
// RefreshDownloadUrl 重新刷新下载链接
|
||||
func (wer *Worker) RefreshDownloadUrl() {
|
||||
var apierr *apierror.ApiError
|
||||
|
||||
logger.Verbosef("get new download url for worker: %d\n", wer.ID())
|
||||
durl, apierr := wer.panClient.OpenapiPanClient().GetFileDownloadUrl(&aliyunpan.GetFileDownloadUrlParam{DriveId: wer.driveId, FileId: wer.fileId})
|
||||
if apierr != nil {
|
||||
logger.Verbosef("get new download url for worker: %d, error: %+v\n", wer.ID(), apierr)
|
||||
wer.status.statusCode = StatusCodeTooManyConnections
|
||||
return
|
||||
}
|
||||
wer.url = durl.Url
|
||||
logger.Verbosef("get new download url for worker: %d, new url: %s\n", wer.ID(), wer.url)
|
||||
}
|
||||
|
||||
// Canceled 是否已经取消
|
||||
@@ -266,6 +269,7 @@ func (wer *Worker) Err() error {
|
||||
|
||||
// Execute 执行任务
|
||||
func (wer *Worker) Execute() {
|
||||
logger.Verbosef("Execute worker: %d\n", wer.ID())
|
||||
wer.lazyInit()
|
||||
|
||||
wer.execMu.Lock()
|
||||
|
||||
@@ -16,12 +16,13 @@ package uploader
|
||||
import "fmt"
|
||||
|
||||
var (
|
||||
UploadUrlExpired = fmt.Errorf("UrlExpired")
|
||||
UploadPartNotSeq = fmt.Errorf("PartNotSequential")
|
||||
UploadNoSuchUpload = fmt.Errorf("NoSuchUpload")
|
||||
UploadTerminate = fmt.Errorf("UploadErrorTerminate")
|
||||
UploadPartAlreadyExist = fmt.Errorf("PartAlreadyExist")
|
||||
UploadHttpError = fmt.Errorf("HttpError")
|
||||
UploadUrlExpired = fmt.Errorf("UrlExpired")
|
||||
UploadPartNotSeq = fmt.Errorf("PartNotSequential")
|
||||
UploadNoSuchUpload = fmt.Errorf("NoSuchUpload")
|
||||
UploadTerminate = fmt.Errorf("UploadErrorTerminate")
|
||||
UploadPartAlreadyExist = fmt.Errorf("PartAlreadyExist")
|
||||
UploadHttpError = fmt.Errorf("HttpError")
|
||||
UploadLocalFileAlreadyClosedError = fmt.Errorf("LocalFileAlreadyClosedError")
|
||||
)
|
||||
|
||||
type (
|
||||
|
||||
@@ -35,10 +35,17 @@ type (
|
||||
workerList []*worker
|
||||
)
|
||||
|
||||
// Readed 获取已上传大小
|
||||
func (werl *workerList) Readed() int64 {
|
||||
var readed int64
|
||||
for _, wer := range *werl {
|
||||
readed += wer.splitUnit.Readed()
|
||||
if wer.uploadDone {
|
||||
// 已经上传完成的分片
|
||||
readed += wer.splitUnit.Range().End - wer.splitUnit.Range().Begin
|
||||
} else {
|
||||
// 正在上传中的分片
|
||||
readed += wer.splitUnit.Readed()
|
||||
}
|
||||
}
|
||||
return readed
|
||||
}
|
||||
@@ -143,9 +150,10 @@ func (muer *MultiUploader) upload() (uperr error) {
|
||||
}()
|
||||
wg.Wait()
|
||||
if uperr != nil {
|
||||
if errors.Is(uperr, UploadPartNotSeq) {
|
||||
// 分片出现乱序,停止上传
|
||||
// 清空数据,准备重新上传
|
||||
if errors.Is(uperr, UploadPartNotSeq) ||
|
||||
errors.Is(uperr, UploadNoSuchUpload) ||
|
||||
errors.Is(uperr, UploadLocalFileAlreadyClosedError) {
|
||||
// 清空数据,停止上传
|
||||
uploadDeque = lane.NewDeque() // 清空待上传列表
|
||||
}
|
||||
}
|
||||
@@ -158,8 +166,10 @@ func (muer *MultiUploader) upload() (uperr error) {
|
||||
// 释放链路
|
||||
uploadClient.CloseIdleConnections()
|
||||
|
||||
// 返回错误,通知上层客户端
|
||||
if errors.Is(uperr, UploadPartNotSeq) {
|
||||
// 返回错误,通知上层客户端进行处理
|
||||
if errors.Is(uperr, UploadPartNotSeq) ||
|
||||
errors.Is(uperr, UploadNoSuchUpload) ||
|
||||
errors.Is(uperr, UploadLocalFileAlreadyClosedError) {
|
||||
return uperr
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,15 @@ package pandownload
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
@@ -26,24 +35,20 @@ import (
|
||||
"github.com/tickstep/aliyunpan/internal/log"
|
||||
"github.com/tickstep/aliyunpan/internal/plugins"
|
||||
"github.com/tickstep/aliyunpan/internal/taskframework"
|
||||
"github.com/tickstep/aliyunpan/internal/ui"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/aliyunpan/library/requester/transfer"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/library-go/requester/rio/speeds"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
// DownloadTaskUnit 下载的任务单元
|
||||
DownloadTaskUnit struct {
|
||||
taskInfo *taskframework.TaskInfo // 任务信息
|
||||
// DownloadActionId 下载动作ID,唯一标识一次下载动作的ID。这个ID每次下载任务启动的时候会自动生成,同一次下载任务下载的文件,这个ID都是同一个值。
|
||||
DownloadActionId string
|
||||
taskInfo *taskframework.TaskInfo // 任务信息
|
||||
|
||||
Cfg *downloader.Config
|
||||
PanClient *config.PanClient
|
||||
@@ -55,7 +60,6 @@ type (
|
||||
|
||||
// 可选项
|
||||
VerbosePrinter *logger.CmdVerbose
|
||||
PrintFormat string
|
||||
IsPrintStatus bool // 是否输出各个下载线程的详细信息
|
||||
IsExecutedPermission bool // 下载成功后是否加上执行权限
|
||||
IsOverwrite bool // 是否覆盖已存在的文件
|
||||
@@ -71,6 +75,8 @@ type (
|
||||
|
||||
// 下载文件记录器
|
||||
FileRecorder *log.FileRecorder
|
||||
|
||||
UI *ui.DashboardPanel // 下载统计面板UI
|
||||
}
|
||||
)
|
||||
|
||||
@@ -107,6 +113,26 @@ func (dtu *DownloadTaskUnit) verboseInfof(format string, a ...interface{}) {
|
||||
}
|
||||
}
|
||||
|
||||
// logf 打印文本日志
|
||||
func (dtu *DownloadTaskUnit) logf(format string, a ...interface{}) {
|
||||
if dtu.UI != nil {
|
||||
// 使用UI面板显示下载日志
|
||||
dtu.UI.Logf(format, a...)
|
||||
return
|
||||
}
|
||||
|
||||
// 使用传统单行文本显示下载日志,直接输出到控制台
|
||||
fmt.Printf(format, a...)
|
||||
}
|
||||
|
||||
// updateUITaskState 更新UI下载面板的任务状态
|
||||
func (dtu *DownloadTaskUnit) updateUITaskState(state ui.TaskState, message string) {
|
||||
if dtu.UI == nil {
|
||||
return
|
||||
}
|
||||
dtu.UI.MarkTaskState(dtu.taskInfo.Id(), state, message)
|
||||
}
|
||||
|
||||
// download 执行下载文件(非目录)
|
||||
func (dtu *DownloadTaskUnit) download() (err error) {
|
||||
var (
|
||||
@@ -178,14 +204,17 @@ func (dtu *DownloadTaskUnit) download() (err error) {
|
||||
return apierror.NewFailedApiError("")
|
||||
})
|
||||
|
||||
// 检查输出格式
|
||||
if dtu.PrintFormat == "" {
|
||||
dtu.PrintFormat = DefaultPrintFormat
|
||||
}
|
||||
|
||||
// 这里用共享变量的方式
|
||||
isComplete := false
|
||||
der.OnDownloadStatusEvent(func(status transfer.DownloadStatuser, workersCallback func(downloader.RangeWorkerFunc)) {
|
||||
// 方式1:使用UI统计面板显示下载进度
|
||||
if dtu.UI != nil {
|
||||
// 更新UI面板任务状态
|
||||
dtu.UI.UpdateTaskProgress(dtu.taskInfo.Id(), status.Downloaded(), status.TotalSize(), status.SpeedsPerSecond(), status.TimeLeft())
|
||||
return
|
||||
}
|
||||
|
||||
// 方式2:使用传统单行显示下载进度
|
||||
// 这里可能会下载结束了, 还会输出内容
|
||||
builder := &strings.Builder{}
|
||||
if dtu.IsPrintStatus {
|
||||
@@ -233,7 +262,7 @@ func (dtu *DownloadTaskUnit) download() (err error) {
|
||||
})
|
||||
|
||||
der.OnExecute(func() {
|
||||
fmt.Printf("[%s] 下载开始\n", dtu.taskInfo.Id())
|
||||
dtu.logf("[%s] 下载开始\n", dtu.taskInfo.Id())
|
||||
})
|
||||
|
||||
err = der.Execute()
|
||||
@@ -250,7 +279,7 @@ func (dtu *DownloadTaskUnit) download() (err error) {
|
||||
if removeErr != nil {
|
||||
dtu.verboseInfof("[%s] remove file error: %s\n", dtu.taskInfo.Id(), removeErr)
|
||||
}
|
||||
fmt.Printf("[%s] 下载失败,文件不合法或者被禁止下载: %s\n", dtu.taskInfo.Id(), dtu.SavePath)
|
||||
dtu.logf("[%s] 下载失败,文件不合法或者被禁止下载: %s\n", dtu.taskInfo.Id(), dtu.SavePath)
|
||||
return err
|
||||
} else {
|
||||
// 下载发生错误
|
||||
@@ -275,10 +304,10 @@ func (dtu *DownloadTaskUnit) download() (err error) {
|
||||
if dtu.IsExecutedPermission {
|
||||
err = file.Chmod(0766)
|
||||
if err != nil {
|
||||
fmt.Printf("[%s] 警告, 加执行权限错误: %s\n", dtu.taskInfo.Id(), err)
|
||||
dtu.logf("[%s] 警告, 加执行权限错误: %s\n", dtu.taskInfo.Id(), err)
|
||||
}
|
||||
}
|
||||
fmt.Printf("\n[%s] 下载完成, 保存位置: %s\n", dtu.taskInfo.Id(), dtu.SavePath)
|
||||
dtu.logf("\n[%s] 下载完成, 保存位置: %s\n", dtu.taskInfo.Id(), dtu.SavePath)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -318,7 +347,7 @@ func (dtu *DownloadTaskUnit) checkFileValid(result *taskframework.TaskUnitRunRes
|
||||
|
||||
if dtu.fileInfo.FileSize >= 128*converter.MB {
|
||||
// 大文件, 输出一句提示消息
|
||||
fmt.Printf("[%s] 开始检验文件有效性, 请稍候...\n", dtu.taskInfo.Id())
|
||||
dtu.logf("[%s] 开始检验文件有效性, 请稍候...\n", dtu.taskInfo.Id())
|
||||
}
|
||||
|
||||
// 就在这里处理校验出错
|
||||
@@ -331,7 +360,7 @@ func (dtu *DownloadTaskUnit) checkFileValid(result *taskframework.TaskUnitRunRes
|
||||
// 文件不支持校验
|
||||
result.ResultMessage = "检验文件有效性"
|
||||
result.Err = err
|
||||
fmt.Printf("[%s] 检验文件有效性: %s\n", dtu.taskInfo.Id(), err)
|
||||
dtu.logf("[%s] 检验文件有效性: %s\n", dtu.taskInfo.Id(), err)
|
||||
return true
|
||||
case ErrDownloadFileBanned:
|
||||
// 违规文件
|
||||
@@ -349,7 +378,7 @@ func (dtu *DownloadTaskUnit) checkFileValid(result *taskframework.TaskUnitRunRes
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("[%s] 检验文件有效性成功: %s\n", dtu.taskInfo.Id(), dtu.SavePath)
|
||||
dtu.logf("[%s] 检验文件有效性成功: %s\n", dtu.taskInfo.Id(), dtu.SavePath)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -357,16 +386,19 @@ func (dtu *DownloadTaskUnit) OnRetry(lastRunResult *taskframework.TaskUnitRunRes
|
||||
// 输出错误信息
|
||||
if lastRunResult.Err == nil {
|
||||
// result中不包含Err, 忽略输出
|
||||
fmt.Printf("[%s] %s, 重试 %d/%d\n", dtu.taskInfo.Id(), lastRunResult.ResultMessage, dtu.taskInfo.Retry(), dtu.taskInfo.MaxRetry())
|
||||
dtu.logf("[%s] %s, 重试 %d/%d\n", dtu.taskInfo.Id(), lastRunResult.ResultMessage, dtu.taskInfo.Retry(), dtu.taskInfo.MaxRetry())
|
||||
return
|
||||
}
|
||||
fmt.Printf("[%s] %s, %s, 重试 %d/%d\n", dtu.taskInfo.Id(), lastRunResult.ResultMessage, lastRunResult.Err, dtu.taskInfo.Retry(), dtu.taskInfo.MaxRetry())
|
||||
dtu.logf("[%s] %s, %s, 重试 %d/%d\n", dtu.taskInfo.Id(), lastRunResult.ResultMessage, lastRunResult.Err, dtu.taskInfo.Retry(), dtu.taskInfo.MaxRetry())
|
||||
}
|
||||
|
||||
func (dtu *DownloadTaskUnit) OnSuccess(lastRunResult *taskframework.TaskUnitRunResult) {
|
||||
// 执行插件
|
||||
dtu.pluginCallback("success")
|
||||
|
||||
// 更新UI面板任务状态
|
||||
dtu.updateUITaskState(ui.TaskSuccess, "")
|
||||
|
||||
// 下载文件数据记录
|
||||
if config.Config.FileRecordConfig == "1" {
|
||||
if dtu.fileInfo.IsFile() {
|
||||
@@ -386,13 +418,20 @@ func (dtu *DownloadTaskUnit) OnFailed(lastRunResult *taskframework.TaskUnitRunRe
|
||||
// 失败
|
||||
dtu.pluginCallback("fail")
|
||||
|
||||
// 更新UI面板
|
||||
failedMessage := lastRunResult.ResultMessage
|
||||
if lastRunResult.Err != nil {
|
||||
failedMessage = fmt.Sprintf("%s, %s", lastRunResult.ResultMessage, lastRunResult.Err)
|
||||
}
|
||||
dtu.updateUITaskState(ui.TaskFailed, failedMessage)
|
||||
|
||||
// 失败
|
||||
if lastRunResult.Err == nil {
|
||||
// result中不包含Err, 忽略输出
|
||||
fmt.Printf("[%s] %s\n", dtu.taskInfo.Id(), lastRunResult.ResultMessage)
|
||||
dtu.logf("[%s] %s\n", dtu.taskInfo.Id(), lastRunResult.ResultMessage)
|
||||
return
|
||||
}
|
||||
fmt.Printf("[%s] %s, %s\n", dtu.taskInfo.Id(), lastRunResult.ResultMessage, lastRunResult.Err)
|
||||
dtu.logf("[%s] %s, %s\n", dtu.taskInfo.Id(), lastRunResult.ResultMessage, lastRunResult.Err)
|
||||
}
|
||||
|
||||
func (dtu *DownloadTaskUnit) pluginCallback(result string) {
|
||||
@@ -402,7 +441,9 @@ func (dtu *DownloadTaskUnit) pluginCallback(result string) {
|
||||
pluginManger := plugins.NewPluginManager(config.GetPluginDir())
|
||||
plugin, _ := pluginManger.GetPlugin()
|
||||
pluginParam := &plugins.DownloadFileFinishParams{
|
||||
DownloadActionId: dtu.DownloadActionId,
|
||||
DriveId: dtu.fileInfo.DriveId,
|
||||
DriveFileId: dtu.fileInfo.FileId,
|
||||
DriveFilePath: dtu.fileInfo.Path,
|
||||
DriveFileName: dtu.fileInfo.FileName,
|
||||
DriveFileSize: dtu.fileInfo.FileSize,
|
||||
@@ -423,7 +464,7 @@ func (dtu *DownloadTaskUnit) OnComplete(lastRunResult *taskframework.TaskUnitRun
|
||||
}
|
||||
|
||||
func (dtu *DownloadTaskUnit) OnCancel(lastRunResult *taskframework.TaskUnitRunResult) {
|
||||
|
||||
dtu.updateUITaskState(ui.TaskCanceled, "取消: "+dtu.FilePanPath)
|
||||
}
|
||||
|
||||
func (dtu *DownloadTaskUnit) RetryWait() time.Duration {
|
||||
@@ -450,8 +491,8 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
||||
}
|
||||
|
||||
// 输出文件信息
|
||||
fmt.Print("\n")
|
||||
fmt.Printf("[%s] ----\n%s\n", dtu.taskInfo.Id(), dtu.fileInfo.String())
|
||||
dtu.logf("\n")
|
||||
dtu.logf("[%s] ----\n%s\n", dtu.taskInfo.Id(), dtu.fileInfo.String())
|
||||
|
||||
// 调用插件
|
||||
ft := "file"
|
||||
@@ -463,6 +504,7 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
||||
localFilePath := strings.TrimPrefix(dtu.SavePath, dtu.OriginSaveRootPath)
|
||||
localFilePath = strings.TrimPrefix(strings.TrimPrefix(localFilePath, "\\"), "/")
|
||||
pluginParam := &plugins.DownloadFilePrepareParams{
|
||||
DownloadActionId: dtu.DownloadActionId,
|
||||
DriveId: dtu.fileInfo.DriveId,
|
||||
DriveFilePath: dtu.fileInfo.Path,
|
||||
DriveFileName: dtu.fileInfo.FileName,
|
||||
@@ -475,7 +517,8 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
||||
if downloadFilePrepareResult, er := plugin.DownloadFilePrepareCallback(plugins.GetContext(config.Config.ActiveUser()), pluginParam); er == nil && downloadFilePrepareResult != nil {
|
||||
if strings.Compare("yes", downloadFilePrepareResult.DownloadApproved) != 0 {
|
||||
// skip download this file
|
||||
fmt.Printf("插件取消了该文件下载: %s\n", dtu.fileInfo.Path)
|
||||
dtu.logf("插件取消了该文件下载: %s\n", dtu.fileInfo.Path)
|
||||
dtu.updateUITaskState(ui.TaskCanceled, "插件取消了该文件下载: "+dtu.fileInfo.Path)
|
||||
result.Succeed = false
|
||||
result.Cancel = true
|
||||
return
|
||||
@@ -484,7 +527,7 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
||||
targetSaveRelativePath := strings.TrimPrefix(downloadFilePrepareResult.LocalFilePath, "/")
|
||||
targetSaveRelativePath = strings.TrimPrefix(targetSaveRelativePath, "\\")
|
||||
dtu.SavePath = path.Clean(dtu.OriginSaveRootPath + string(os.PathSeparator) + targetSaveRelativePath)
|
||||
fmt.Printf("插件修改文件下载保存路径为: %s\n", dtu.SavePath)
|
||||
dtu.logf("插件修改文件下载保存路径为: %s\n", dtu.SavePath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -547,13 +590,17 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
||||
}
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// 排序,按名称排序,从小到大
|
||||
sort.Slice(fileList, func(i, j int) bool {
|
||||
return fileList[i].FileName < fileList[j].FileName
|
||||
})
|
||||
// 创建对应的任务进行下载
|
||||
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)
|
||||
dtu.logf("排除文件: %s\n", fileList[k].Path)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -573,7 +620,11 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
||||
|
||||
// 加入父队列,按照队列调度进行下载
|
||||
info := dtu.ParentTaskExecutor.Append(&subUnit, dtu.taskInfo.MaxRetry())
|
||||
fmt.Printf("[%s] 加入下载队列: %s\n", info.Id(), fileList[k].Path)
|
||||
dtu.logf("[%s] 加入下载队列: %s\n", info.Id(), fileList[k].Path)
|
||||
// UI面板注册任务
|
||||
if dtu.UI != nil {
|
||||
dtu.UI.RegisterTask(info.Id(), fileList[k].Path, fileList[k].FileSize, fileList[k].IsFile())
|
||||
}
|
||||
}
|
||||
|
||||
// 本下载任务执行成功
|
||||
@@ -581,7 +632,7 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("[%s] 准备下载: %s\n", dtu.taskInfo.Id(), dtu.FilePanPath)
|
||||
dtu.logf("[%s] 准备下载: %s\n", dtu.taskInfo.Id(), dtu.FilePanPath)
|
||||
|
||||
//if !dtu.IsOverwrite && FileExist(dtu.SavePath) {
|
||||
// fmt.Printf("[%s] 文件已经存在: %s, 跳过...\n", dtu.taskInfo.Id(), dtu.SavePath)
|
||||
@@ -590,12 +641,15 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
||||
//}
|
||||
// 支持符号文件,逻辑和注释代码一致
|
||||
if !dtu.IsOverwrite && SymlinkFileExist(dtu.SavePath, dtu.OriginSaveRootPath) {
|
||||
fmt.Printf("[%s] 文件已经存在: %s, 跳过...\n", dtu.taskInfo.Id(), dtu.SavePath)
|
||||
dtu.updateUITaskState(ui.TaskSkipped, "文件已存在,跳过: "+dtu.SavePath)
|
||||
dtu.logf("[%s] 文件已经存在: %s, 跳过...\n", dtu.taskInfo.Id(), dtu.SavePath)
|
||||
result.Succeed = true // 执行成功
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("[%s] 将会下载到路径: %s\n", dtu.taskInfo.Id(), dtu.SavePath)
|
||||
dtu.logf("[%s] 将会下载到路径: %s\n", dtu.taskInfo.Id(), dtu.SavePath)
|
||||
// 更新UI面板任务状态
|
||||
dtu.updateUITaskState(ui.TaskRunning, "")
|
||||
|
||||
var ok bool
|
||||
er := dtu.download()
|
||||
|
||||
@@ -4,23 +4,40 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan/internal/global"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/library-go/crypto"
|
||||
"github.com/tickstep/library-go/getip"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/library-go/requester"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tickstep/aliyunpan/internal/global"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/library-go/crypto"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/library-go/requester"
|
||||
)
|
||||
|
||||
type LoginHelper struct {
|
||||
webHost string
|
||||
}
|
||||
|
||||
const (
|
||||
// SUCCESS 成功
|
||||
SUCCESS = 0
|
||||
// ERROR 失败
|
||||
ERROR = 500
|
||||
// ERROR_TOO_MANY_REQUESTS 请求太频繁,已被限流
|
||||
ERROR_TOO_MANY_REQUESTS = 429
|
||||
// ERROR_NEED_LOGIN_AGAIN Token已经过期,需要重新登录
|
||||
ERROR_NEED_LOGIN_AGAIN = 100004
|
||||
)
|
||||
|
||||
// CommonResultError 通用接口错误
|
||||
type CommonResultError struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
type LoginHttpResult struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
@@ -51,6 +68,13 @@ type CommonTokenEntity struct {
|
||||
Webapi *LoginTokenResult `json:"webapi"`
|
||||
}
|
||||
|
||||
func NewCommonResultError(msg string) *CommonResultError {
|
||||
return &CommonResultError{
|
||||
Code: 500,
|
||||
Msg: msg,
|
||||
}
|
||||
}
|
||||
|
||||
func NewLoginHelper(webHost string) *LoginHelper {
|
||||
return &LoginHelper{
|
||||
webHost: webHost,
|
||||
@@ -63,7 +87,7 @@ func (h *LoginHelper) GetQRCodeLoginUrl(keyStr string) (*QRCodeUrlResult, error)
|
||||
keyStr = utils.GetUniqueKeyStr()
|
||||
}
|
||||
fullUrl := strings.Builder{}
|
||||
ipAddr, err := getip.IPInfoFromTechainBaidu()
|
||||
ipAddr, err := utils.GetPublicIP()
|
||||
if err != nil {
|
||||
ipAddr = "127.0.0.1"
|
||||
}
|
||||
@@ -86,6 +110,16 @@ func (h *LoginHelper) GetQRCodeLoginUrl(keyStr string) (*QRCodeUrlResult, error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 通用接口错误
|
||||
commonResult := &CommonResultError{}
|
||||
if err1 := json.Unmarshal(body, commonResult); err1 != nil {
|
||||
logger.Verboseln("parse openapi result json error ", err1)
|
||||
return nil, err1
|
||||
}
|
||||
if commonResult.Code != 0 {
|
||||
return nil, fmt.Errorf(commonResult.Msg)
|
||||
}
|
||||
|
||||
errResp := &LoginHttpResult{}
|
||||
if err1 := json.Unmarshal(body, errResp); err1 != nil {
|
||||
logger.Verboseln("parse qrcode result json error ", err1)
|
||||
@@ -137,6 +171,16 @@ func (h *LoginHelper) GetQRCodeLoginResult(tokenId string) (*QRCodeLoginResult,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 通用接口错误
|
||||
commonResult := &CommonResultError{}
|
||||
if err1 := json.Unmarshal(body, commonResult); err1 != nil {
|
||||
logger.Verboseln("parse openapi result json error ", err1)
|
||||
return nil, err1
|
||||
}
|
||||
if commonResult.Code != 0 {
|
||||
return nil, fmt.Errorf(commonResult.Msg)
|
||||
}
|
||||
|
||||
errResp := &LoginHttpResult{}
|
||||
if err1 := json.Unmarshal(body, errResp); err1 != nil {
|
||||
logger.Verboseln("parse qrcode result json error ", err1)
|
||||
@@ -177,6 +221,16 @@ func (h *LoginHelper) GetRefreshToken(tokenId string) (*QRCodeLoginResult, error
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 通用接口错误
|
||||
commonResult := &CommonResultError{}
|
||||
if err1 := json.Unmarshal(body, commonResult); err1 != nil {
|
||||
logger.Verboseln("parse openapi result json error ", err1)
|
||||
return nil, err1
|
||||
}
|
||||
if commonResult.Code != 0 {
|
||||
return nil, fmt.Errorf(commonResult.Msg)
|
||||
}
|
||||
|
||||
errResp := &LoginHttpResult{}
|
||||
if err1 := json.Unmarshal(body, errResp); err1 != nil {
|
||||
logger.Verboseln("parse qrcode result json error ", err1)
|
||||
@@ -233,7 +287,7 @@ func (h *LoginHelper) ParseSecureRefreshToken(keyStr, secureRefreshToken string)
|
||||
}
|
||||
|
||||
// GetWebapiNewToken 获取Webapi Token
|
||||
func (h *LoginHelper) GetWebapiNewToken(ticketId, userId, oldAccessToken string) (*LoginTokenResult, error) {
|
||||
func (h *LoginHelper) GetWebapiNewToken(ticketId, userId, oldAccessToken string) (*LoginTokenResult, *CommonResultError) {
|
||||
fullUrl := strings.Builder{}
|
||||
fmt.Fprintf(&fullUrl, "%s/auth/tickstep/aliyunpan/token/webapi/%s/refresh?userId=%s",
|
||||
h.webHost, ticketId, userId)
|
||||
@@ -251,16 +305,29 @@ func (h *LoginHelper) GetWebapiNewToken(ticketId, userId, oldAccessToken string)
|
||||
body, err := client.Fetch("GET", fullUrl.String(), nil, header)
|
||||
if err != nil {
|
||||
logger.Verboseln("get web token result error ", err)
|
||||
return nil, err
|
||||
return nil, NewCommonResultError(err.Error())
|
||||
}
|
||||
|
||||
// 通用接口错误
|
||||
commonResult := &CommonResultError{}
|
||||
if err1 := json.Unmarshal(body, commonResult); err1 != nil {
|
||||
logger.Verboseln("parse openapi result json error ", err1)
|
||||
return nil, NewCommonResultError(err1.Error())
|
||||
}
|
||||
if commonResult.Code != 0 {
|
||||
return nil, commonResult
|
||||
}
|
||||
|
||||
errResp := &LoginHttpResult{}
|
||||
if err1 := json.Unmarshal(body, errResp); err1 != nil {
|
||||
logger.Verboseln("parse result json error ", err1)
|
||||
return nil, err1
|
||||
return nil, NewCommonResultError(err1.Error())
|
||||
}
|
||||
if errResp.Code != 0 {
|
||||
return nil, fmt.Errorf(errResp.Msg)
|
||||
return nil, &CommonResultError{
|
||||
Code: errResp.Code,
|
||||
Msg: errResp.Msg,
|
||||
}
|
||||
}
|
||||
|
||||
// parse result
|
||||
@@ -268,13 +335,13 @@ func (h *LoginHelper) GetWebapiNewToken(ticketId, userId, oldAccessToken string)
|
||||
r.Data = &LoginTokenResult{}
|
||||
if err2 := json.Unmarshal(body, r); err2 != nil {
|
||||
logger.Verboseln("parse web token result json error ", err2)
|
||||
return nil, err2
|
||||
return nil, NewCommonResultError(err2.Error())
|
||||
}
|
||||
return r.Data.(*LoginTokenResult), nil
|
||||
}
|
||||
|
||||
// GetOpenapiNewToken 获取Openapi Token
|
||||
func (h *LoginHelper) GetOpenapiNewToken(ticketId, userId, oldAccessToken string) (*LoginTokenResult, error) {
|
||||
func (h *LoginHelper) GetOpenapiNewToken(ticketId, userId, oldAccessToken string) (*LoginTokenResult, *CommonResultError) {
|
||||
fullUrl := strings.Builder{}
|
||||
fmt.Fprintf(&fullUrl, "%s/auth/tickstep/aliyunpan/token/openapi/%s/refresh?userId=%s",
|
||||
h.webHost, ticketId, userId)
|
||||
@@ -292,16 +359,30 @@ func (h *LoginHelper) GetOpenapiNewToken(ticketId, userId, oldAccessToken string
|
||||
body, err := client.Fetch("GET", fullUrl.String(), nil, header)
|
||||
if err != nil {
|
||||
logger.Verboseln("get openapi token result error ", err)
|
||||
return nil, err
|
||||
return nil, NewCommonResultError(err.Error())
|
||||
}
|
||||
|
||||
// 通用接口错误
|
||||
commonResult := &CommonResultError{}
|
||||
if err1 := json.Unmarshal(body, commonResult); err1 != nil {
|
||||
logger.Verboseln("parse openapi result json error ", err1)
|
||||
return nil, NewCommonResultError(err1.Error())
|
||||
}
|
||||
if commonResult.Code != 0 {
|
||||
return nil, commonResult
|
||||
}
|
||||
|
||||
// 解析结果
|
||||
errResp := &LoginHttpResult{}
|
||||
if err1 := json.Unmarshal(body, errResp); err1 != nil {
|
||||
logger.Verboseln("parse result json error ", err1)
|
||||
return nil, err1
|
||||
return nil, NewCommonResultError(err1.Error())
|
||||
}
|
||||
if errResp.Code != 0 {
|
||||
return nil, fmt.Errorf(errResp.Msg)
|
||||
return nil, &CommonResultError{
|
||||
Code: errResp.Code,
|
||||
Msg: errResp.Msg,
|
||||
}
|
||||
}
|
||||
|
||||
// parse result
|
||||
@@ -309,7 +390,7 @@ func (h *LoginHelper) GetOpenapiNewToken(ticketId, userId, oldAccessToken string
|
||||
r.Data = &LoginTokenResult{}
|
||||
if err2 := json.Unmarshal(body, r); err2 != nil {
|
||||
logger.Verboseln("parse openapi token result json error ", err2)
|
||||
return nil, err2
|
||||
return nil, NewCommonResultError(err2.Error())
|
||||
}
|
||||
return r.Data.(*LoginTokenResult), nil
|
||||
}
|
||||
@@ -335,6 +416,16 @@ func (h *LoginHelper) GetLoginToken(ticketId string) (*CommonTokenEntity, error)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 通用接口错误
|
||||
commonResult := &CommonResultError{}
|
||||
if err1 := json.Unmarshal(body, commonResult); err1 != nil {
|
||||
logger.Verboseln("parse openapi result json error ", err1)
|
||||
return nil, err1
|
||||
}
|
||||
if commonResult.Code != 0 {
|
||||
return nil, fmt.Errorf(commonResult.Msg)
|
||||
}
|
||||
|
||||
errResp := &LoginHttpResult{}
|
||||
if err1 := json.Unmarshal(body, errResp); err1 != nil {
|
||||
logger.Verboseln("parse result json error ", err1)
|
||||
|
||||
@@ -16,13 +16,18 @@ package panupload
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/library-go/requester"
|
||||
"io"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
@@ -72,6 +77,11 @@ func (pu *PanUpload) Precreate() (err error) {
|
||||
}
|
||||
|
||||
func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int64, partEnd int64, r rio.ReaderLen64, uploadClient *requester.HTTPClient) (uploadDone bool, uperr error) {
|
||||
if len(pu.uploadOpEntity.PartInfoList) == 0 {
|
||||
logger.Verbosef("网盘上传参数错误,分片%d => uploadOpEntity.PartInfoList为空\n", partseq+1)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
pu.lazyInit()
|
||||
|
||||
// check url expired or not
|
||||
@@ -102,6 +112,7 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
|
||||
}
|
||||
|
||||
var respErr *uploader.MultiError
|
||||
// 分片上传回调函数,供阿里云盘API接口使用
|
||||
uploadFunc := func(httpMethod, fullUrl string, headers map[string]string) (*http.Response, error) {
|
||||
var resp *http.Response
|
||||
var respError error = nil
|
||||
@@ -124,10 +135,10 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
|
||||
if blen > 0 {
|
||||
buf := make([]byte, blen)
|
||||
resp.Body.Read(buf)
|
||||
logger.Verbosef("分片上传出错: 分片%d => %s\n", partseq+1, string(buf))
|
||||
logger.Verbosef("分片上传出错: 分片%d, 错误信息: %s\n", partseq+1, string(buf))
|
||||
|
||||
errResp := &apierror.ErrorXmlResp{}
|
||||
if err := xml.Unmarshal(buf, errResp); err == nil {
|
||||
if errXml := xml.Unmarshal(buf, errResp); errXml == nil {
|
||||
if errResp.Code != "" {
|
||||
if "PartNotSequential" == errResp.Code {
|
||||
respError = uploader.UploadPartNotSeq
|
||||
@@ -184,10 +195,31 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 默认错误
|
||||
respError = uploader.UploadTerminate
|
||||
respErr = &uploader.MultiError{
|
||||
Terminated: true,
|
||||
}
|
||||
|
||||
// 解析HTTP请求出现的错误信息
|
||||
if err != nil {
|
||||
var netErr *url.Error
|
||||
if errors.As(err, &netErr) {
|
||||
var pathErr *os.PathError
|
||||
if errors.As(netErr.Err, &pathErr) { // 本地文件读取异常
|
||||
if strings.Contains(pathErr.Err.Error(), "file already closed") {
|
||||
// 本地文件流触发"file already closed"错误
|
||||
// 一般是文件上传过程中,文件被删除了,或者文件所在的硬盘异常导致文件流被系统关闭
|
||||
respError = uploader.UploadLocalFileAlreadyClosedError
|
||||
respErr = &uploader.MultiError{
|
||||
Terminated: true,
|
||||
NeedStartOver: false,
|
||||
Err: uploader.UploadLocalFileAlreadyClosedError,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return resp, respError
|
||||
}
|
||||
@@ -197,7 +229,7 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
|
||||
apiError := pu.panClient.OpenapiPanClient().UploadFileData(uploadUrl, uploadFunc)
|
||||
|
||||
if respErr != nil {
|
||||
if respErr.Err == uploader.UploadUrlExpired {
|
||||
if errors.Is(respErr.Err, uploader.UploadUrlExpired) {
|
||||
// URL过期,获取新的URL
|
||||
guur, er := pu.panClient.OpenapiPanClient().GetUploadUrl(&aliyunpan.GetUploadUrlParam{
|
||||
DriveId: pu.driveId,
|
||||
@@ -213,16 +245,19 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
|
||||
|
||||
// 获取新的上传URL重试一次
|
||||
pu.uploadOpEntity.PartInfoList[partseq] = guur.PartInfoList[0]
|
||||
uploadUrl := pu.uploadOpEntity.PartInfoList[partseq].UploadURL
|
||||
uploadUrl = pu.uploadOpEntity.PartInfoList[partseq].UploadURL
|
||||
apiError = pu.panClient.OpenapiPanClient().UploadFileData(uploadUrl, uploadFunc)
|
||||
} else if respErr.Err == uploader.UploadPartAlreadyExist {
|
||||
} else if errors.Is(respErr.Err, uploader.UploadPartAlreadyExist) {
|
||||
// already upload
|
||||
// success
|
||||
return true, nil
|
||||
} else if respErr.Err == uploader.UploadPartNotSeq {
|
||||
} else if errors.Is(respErr.Err, uploader.UploadPartNotSeq) {
|
||||
// 上传分片乱序了
|
||||
// 返回错误,然后重传
|
||||
return false, respErr
|
||||
} else if errors.Is(respErr.Err, uploader.UploadLocalFileAlreadyClosedError) {
|
||||
// 本地文件读取错误
|
||||
return false, respErr
|
||||
} else {
|
||||
return false, respErr
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ package panupload
|
||||
import (
|
||||
"errors"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -48,6 +49,7 @@ type (
|
||||
func NewUploadingDatabase() (ud *UploadingDatabase, err error) {
|
||||
file, err := os.OpenFile(filepath.Join(config.GetConfigDir(), UploadingFileName), os.O_CREATE|os.O_RDWR, 0777)
|
||||
if err != nil {
|
||||
// 打开文件错误,一般是文件权限问题
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -65,7 +67,20 @@ func NewUploadingDatabase() (ud *UploadingDatabase, err error) {
|
||||
|
||||
err = jsonhelper.UnmarshalData(file, ud)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// 上传数据库文件内容解析错误
|
||||
// 尝试从备份的文件读取数据
|
||||
bakFile, err1 := os.OpenFile(filepath.Join(config.GetConfigDir(), UploadingBackupFileName), os.O_CREATE|os.O_RDONLY, 0777)
|
||||
if err1 != nil {
|
||||
return nil, err
|
||||
}
|
||||
err2 := jsonhelper.UnmarshalData(bakFile, ud)
|
||||
if err2 != nil {
|
||||
return nil, err
|
||||
}
|
||||
bakFile.Close()
|
||||
// 旧的备份文件可以正常使用,复制备份的数据文件到当前数据文件中
|
||||
ud.copyFile(filepath.Join(config.GetConfigDir(), UploadingFileName), filepath.Join(config.GetConfigDir(), UploadingBackupFileName))
|
||||
return ud, nil
|
||||
}
|
||||
|
||||
return ud, nil
|
||||
@@ -87,11 +102,23 @@ func (ud *UploadingDatabase) Save() error {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// 备份旧的数据库文件
|
||||
// 因为下面有文件内容清空、写入新内容的操作。有小概率出现文件保存没有完成程序就退出的问题,这会导致数据库内容丢失。所以这里必须备份一下旧文件
|
||||
err1 := ud.copyFile(filepath.Join(config.GetConfigDir(), UploadingBackupFileName), filepath.Join(config.GetConfigDir(), UploadingFileName))
|
||||
if err1 != nil {
|
||||
logger.Verboseln("备份上传数据库文件出错: {}", err1)
|
||||
} else {
|
||||
logger.Verboseln("成功备份旧的上传数据库文件")
|
||||
}
|
||||
|
||||
logger.Verboseln("保存最新上传数据库内容")
|
||||
// 清空文件旧内容
|
||||
err = ud.dataFile.Truncate(int64(builder.Len()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 写入新内容
|
||||
str := builder.String()
|
||||
_, err = ud.dataFile.WriteAt(converter.ToBytes(str), 0)
|
||||
if err != nil {
|
||||
@@ -101,6 +128,26 @@ func (ud *UploadingDatabase) Save() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// copyFile 复制文件
|
||||
func (ud *UploadingDatabase) copyFile(dstFilePath string, srcFilePath string) error {
|
||||
os.Remove(dstFilePath)
|
||||
dstFile, err1 := os.OpenFile(dstFilePath, os.O_CREATE|os.O_WRONLY, 0777)
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
srcFile, err2 := os.OpenFile(srcFilePath, os.O_CREATE|os.O_RDONLY, 0777)
|
||||
if err2 != nil {
|
||||
return err2
|
||||
}
|
||||
_, err3 := io.Copy(dstFile, srcFile)
|
||||
if err3 != nil {
|
||||
return err3
|
||||
}
|
||||
dstFile.Close()
|
||||
srcFile.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateUploading 更新正在上传
|
||||
func (ud *UploadingDatabase) UpdateUploading(meta *localfile.LocalFileMeta, state *uploader.InstanceState) {
|
||||
if meta == nil {
|
||||
|
||||
@@ -16,9 +16,6 @@ package panupload
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan/internal/log"
|
||||
"github.com/tickstep/aliyunpan/internal/plugins"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
@@ -26,6 +23,11 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tickstep/aliyunpan/internal/log"
|
||||
"github.com/tickstep/aliyunpan/internal/plugins"
|
||||
"github.com/tickstep/aliyunpan/internal/ui"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/library-go/requester/rio/speeds"
|
||||
|
||||
@@ -79,6 +81,8 @@ type (
|
||||
|
||||
// 上传文件记录器
|
||||
FileRecorder *log.FileRecorder
|
||||
|
||||
UI *ui.DashboardPanel // 上传统计面板UI
|
||||
}
|
||||
)
|
||||
|
||||
@@ -97,6 +101,26 @@ const (
|
||||
StrUploadFailed = "上传文件失败"
|
||||
)
|
||||
|
||||
// logf 打印文本日志
|
||||
func (utu *UploadTaskUnit) logf(format string, a ...interface{}) {
|
||||
if utu.UI != nil {
|
||||
// 使用UI面板显示日志
|
||||
utu.UI.Logf(format, a...)
|
||||
return
|
||||
}
|
||||
|
||||
// 使用传统单行文本显示下载日志,直接输出到控制台
|
||||
fmt.Printf(format, a...)
|
||||
}
|
||||
|
||||
// updateUITaskState 更新UI面板的任务状态
|
||||
func (utu *UploadTaskUnit) updateUITaskState(state ui.TaskState, message string) {
|
||||
if utu.UI == nil {
|
||||
return
|
||||
}
|
||||
utu.UI.MarkTaskState(utu.taskInfo.Id(), state, message)
|
||||
}
|
||||
|
||||
func (utu *UploadTaskUnit) SetTaskInfo(taskInfo *taskframework.TaskInfo) {
|
||||
utu.taskInfo = taskInfo
|
||||
}
|
||||
@@ -127,7 +151,7 @@ func (utu *UploadTaskUnit) prepareFile() {
|
||||
}
|
||||
|
||||
//if utu.LocalFileChecksum.Length > MaxRapidUploadSize {
|
||||
// fmt.Printf("[%s] 文件超过20GB, 无法使用秒传功能, 跳过秒传...\n", utu.taskInfo.Id())
|
||||
// utu.logf("[%s] 文件超过20GB, 无法使用秒传功能, 跳过秒传...\n", utu.taskInfo.Id())
|
||||
// utu.Step = StepUploadUpload
|
||||
// return
|
||||
//}
|
||||
@@ -142,13 +166,13 @@ func (utu *UploadTaskUnit) rapidUpload() (isContinue bool, result *taskframework
|
||||
|
||||
// 是否可以秒传
|
||||
result = &taskframework.TaskUnitRunResult{}
|
||||
fmt.Printf("[%s] %s 检测秒传中, 请稍候...\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
|
||||
utu.logf("[%s] %s 检测秒传中, 请稍候...\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
|
||||
if utu.LocalFileChecksum.UploadOpEntity.RapidUpload {
|
||||
fmt.Printf("[%s] %s 秒传成功, 保存到网盘路径: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.SavePath)
|
||||
utu.logf("[%s] %s 秒传成功, 保存到网盘路径: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.SavePath)
|
||||
result.Succeed = true
|
||||
return false, result
|
||||
} else {
|
||||
fmt.Printf("[%s] %s 秒传失败,开始正常上传文件\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
|
||||
utu.logf("[%s] %s 秒传失败,开始正常上传文件\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
|
||||
result.Succeed = false
|
||||
result.ResultMessage = "文件未曾上传,无法秒传"
|
||||
return true, result
|
||||
@@ -170,6 +194,13 @@ func (utu *UploadTaskUnit) upload() (result *taskframework.TaskUnitRunResult) {
|
||||
MaxRate: config.Config.MaxUploadRate,
|
||||
}, utu.LocalFileChecksum.UploadOpEntity, utu.PanClient, utu.GlobalSpeedsStat)
|
||||
|
||||
// 模拟 file already closed 错误
|
||||
//go func(f *os.File) {
|
||||
// time.Sleep(10 * time.Second)
|
||||
// f.Close()
|
||||
// utu.logf("关闭文件流: %s\n", f.Name())
|
||||
//}(utu.LocalFileChecksum.GetFile())
|
||||
|
||||
// 设置断点续传
|
||||
if utu.state != nil {
|
||||
muer.SetInstanceState(utu.state)
|
||||
@@ -183,6 +214,14 @@ func (utu *UploadTaskUnit) upload() (result *taskframework.TaskUnitRunResult) {
|
||||
default:
|
||||
}
|
||||
|
||||
// 方式1:使用UI统计面板显示进度
|
||||
if utu.UI != nil {
|
||||
// 更新UI面板任务状态
|
||||
utu.UI.UpdateTaskProgress(utu.taskInfo.Id(), status.Uploaded(), status.TotalSize(), status.SpeedsPerSecond(), status.TimeLeft())
|
||||
return
|
||||
}
|
||||
|
||||
// 方式2:使用传统单行显示下载进度
|
||||
if utu.ShowProgress {
|
||||
// 如果上传速度为0, 剩余时间未知, 则用 - 代替
|
||||
var leftStr string
|
||||
@@ -192,8 +231,11 @@ func (utu *UploadTaskUnit) upload() (result *taskframework.TaskUnitRunResult) {
|
||||
} else {
|
||||
leftStr = left.String()
|
||||
}
|
||||
uploadedPercentage := fmt.Sprintf("%.2f%%", float64(status.Uploaded())/float64(status.TotalSize())*100)
|
||||
fmt.Printf("\r[%s] ↑ %s/%s(%s) %s/s(%s/s) in %s, left %s ............", utu.taskInfo.Id(),
|
||||
uploadedPercentage := "0.00%"
|
||||
if status.TotalSize() > 0 {
|
||||
uploadedPercentage = fmt.Sprintf("%.2f%%", float64(status.Uploaded())/float64(status.TotalSize())*100)
|
||||
}
|
||||
utu.logf("\r[%s] ↑ %s/%s(%s) %s/s(%s/s) in %s, left %s ............", utu.taskInfo.Id(),
|
||||
converter.ConvertFileSize(status.Uploaded(), 2),
|
||||
converter.ConvertFileSize(status.TotalSize(), 2),
|
||||
uploadedPercentage,
|
||||
@@ -208,8 +250,8 @@ func (utu *UploadTaskUnit) upload() (result *taskframework.TaskUnitRunResult) {
|
||||
// result
|
||||
result = &taskframework.TaskUnitRunResult{}
|
||||
muer.OnSuccess(func() {
|
||||
fmt.Printf("\n")
|
||||
fmt.Printf("[%s] %s 上传文件成功, 保存到网盘路径: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.SavePath)
|
||||
utu.logf("\n")
|
||||
utu.logf("[%s] %s 上传文件成功, 保存到网盘路径: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.SavePath)
|
||||
// 统计
|
||||
utu.UploadStatistic.AddTotalSize(utu.LocalFileChecksum.Length)
|
||||
utu.UploadingDatabase.Delete(&utu.LocalFileChecksum.LocalFileMeta) // 删除
|
||||
@@ -254,16 +296,19 @@ func (utu *UploadTaskUnit) OnRetry(lastRunResult *taskframework.TaskUnitRunResul
|
||||
// 输出错误信息
|
||||
if lastRunResult.Err == nil {
|
||||
// result中不包含Err, 忽略输出
|
||||
fmt.Printf("[%s] %s, 重试 %d/%d\n", utu.taskInfo.Id(), lastRunResult.ResultMessage, utu.taskInfo.Retry(), utu.taskInfo.MaxRetry())
|
||||
utu.logf("[%s] %s, 重试 %d/%d\n", utu.taskInfo.Id(), lastRunResult.ResultMessage, utu.taskInfo.Retry(), utu.taskInfo.MaxRetry())
|
||||
return
|
||||
}
|
||||
fmt.Printf("[%s] %s, %s, 重试 %d/%d\n", utu.taskInfo.Id(), lastRunResult.ResultMessage, lastRunResult.Err, utu.taskInfo.Retry(), utu.taskInfo.MaxRetry())
|
||||
utu.logf("[%s] %s, %s, 重试 %d/%d\n", utu.taskInfo.Id(), lastRunResult.ResultMessage, lastRunResult.Err, utu.taskInfo.Retry(), utu.taskInfo.MaxRetry())
|
||||
}
|
||||
|
||||
func (utu *UploadTaskUnit) OnSuccess(lastRunResult *taskframework.TaskUnitRunResult) {
|
||||
// 执行插件
|
||||
utu.pluginCallback("success")
|
||||
|
||||
// 更新UI面板状态
|
||||
utu.updateUITaskState(ui.TaskSuccess, "")
|
||||
|
||||
// 上传文件数据记录
|
||||
if config.Config.FileRecordConfig == "1" {
|
||||
utu.FileRecorder.Append(&log.FileRecordItem{
|
||||
@@ -278,6 +323,7 @@ func (utu *UploadTaskUnit) OnSuccess(lastRunResult *taskframework.TaskUnitRunRes
|
||||
func (utu *UploadTaskUnit) OnFailed(lastRunResult *taskframework.TaskUnitRunResult) {
|
||||
// 失败
|
||||
utu.pluginCallback("fail")
|
||||
utu.updateUITaskState(ui.TaskCanceled, "取消: "+utu.LocalFileChecksum.Path.LogicPath)
|
||||
}
|
||||
|
||||
func (utu *UploadTaskUnit) pluginCallback(result string) {
|
||||
@@ -309,7 +355,12 @@ func (utu *UploadTaskUnit) OnComplete(lastRunResult *taskframework.TaskUnitRunRe
|
||||
// 任务结束,可能成功也可能失败
|
||||
}
|
||||
func (utu *UploadTaskUnit) OnCancel(lastRunResult *taskframework.TaskUnitRunResult) {
|
||||
|
||||
// 更新UI面板
|
||||
failedMessage := lastRunResult.ResultMessage
|
||||
if lastRunResult.Err != nil {
|
||||
failedMessage = fmt.Sprintf("%s, %s", lastRunResult.ResultMessage, lastRunResult.Err)
|
||||
}
|
||||
utu.updateUITaskState(ui.TaskCanceled, failedMessage)
|
||||
}
|
||||
func (utu *UploadTaskUnit) RetryWait() time.Duration {
|
||||
return functions.RetryWait(utu.taskInfo.Retry())
|
||||
@@ -318,7 +369,7 @@ func (utu *UploadTaskUnit) RetryWait() time.Duration {
|
||||
func (utu *UploadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
||||
err := utu.LocalFileChecksum.OpenPath()
|
||||
if err != nil {
|
||||
fmt.Printf("[%s] 文件不可读, 错误信息: %s, 跳过...\n", utu.taskInfo.Id(), err)
|
||||
utu.logf("[%s] 文件不可读, 错误信息: %s, 跳过...\n", utu.taskInfo.Id(), err)
|
||||
return
|
||||
}
|
||||
defer utu.LocalFileChecksum.Close() // 关闭文件
|
||||
@@ -326,7 +377,8 @@ func (utu *UploadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
||||
timeStart := time.Now()
|
||||
result = &taskframework.TaskUnitRunResult{}
|
||||
|
||||
fmt.Printf("[%s] %s 准备上传: %s => %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.LocalFileChecksum.Path.LogicPath, utu.SavePath)
|
||||
utu.logf("[%s] %s 准备上传: %s => %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.LocalFileChecksum.Path.LogicPath, utu.SavePath)
|
||||
utu.updateUITaskState(ui.TaskRunning, "")
|
||||
|
||||
defer func() {
|
||||
var msg string
|
||||
@@ -337,7 +389,7 @@ func (utu *UploadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
||||
} else {
|
||||
msg = result.ResultMessage
|
||||
}
|
||||
fmt.Printf("[%s] %s 文件上传结果: %s 耗时 %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), msg, utils.ConvertTime(time.Now().Sub(timeStart)))
|
||||
utu.logf("[%s] %s 文件上传结果: %s 耗时 %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), msg, utils.ConvertTime(time.Now().Sub(timeStart)))
|
||||
}()
|
||||
|
||||
// 准备文件
|
||||
@@ -373,7 +425,7 @@ StepUploadPrepareUpload:
|
||||
saveFilePath = path.Dir(utu.SavePath)
|
||||
if saveFilePath != "/" {
|
||||
utu.FolderCreateMutex.Lock()
|
||||
fmt.Printf("[%s] %s 正在检测和创建云盘文件夹: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), saveFilePath)
|
||||
utu.logf("[%s] %s 正在检测和创建云盘文件夹: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), saveFilePath)
|
||||
fe, apierr1 := utu.PanClient.OpenapiPanClient().FileInfoByPath(utu.DriveId, saveFilePath)
|
||||
time.Sleep(1 * time.Second)
|
||||
needToCreateFolder := false
|
||||
@@ -422,7 +474,8 @@ StepUploadPrepareUpload:
|
||||
if efi != nil && efi.FileId != "" {
|
||||
result.Succeed = true
|
||||
result.Extra = efi
|
||||
fmt.Printf("[%s] %s 检测到同名文件,跳过上传: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.SavePath)
|
||||
utu.logf("[%s] %s 检测到同名文件,跳过上传: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.SavePath)
|
||||
utu.updateUITaskState(ui.TaskSkipped, "")
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -447,7 +500,7 @@ StepUploadPrepareUpload:
|
||||
|
||||
if preHashMatch { // preHashMatch为true,代表该文件可能已经被上传过,能够支持秒传,所以需要进一步计算完整SHA1进行检测是否能秒传
|
||||
// 计算完整文件SHA1
|
||||
fmt.Printf("[%s] %s 正在计算文件SHA1: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.LocalFileChecksum.Path.LogicPath)
|
||||
utu.logf("[%s] %s 正在计算文件SHA1: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.LocalFileChecksum.Path.LogicPath)
|
||||
utu.LocalFileChecksum.Sum(localfile.CHECKSUM_SHA1)
|
||||
sha1Str = utu.LocalFileChecksum.SHA1
|
||||
if utu.LocalFileChecksum.Length == 0 {
|
||||
@@ -467,7 +520,7 @@ StepUploadPrepareUpload:
|
||||
checkNameMode = "auto_rename"
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("[%s] %s 已经禁用秒传检测,直接上传\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
|
||||
utu.logf("[%s] %s 已经禁用秒传检测,直接上传\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
|
||||
sha1Str = ""
|
||||
contentHashName = ""
|
||||
checkNameMode = "auto_rename"
|
||||
@@ -480,7 +533,7 @@ StepUploadPrepareUpload:
|
||||
if strings.ToUpper(efi.ContentHash) == strings.ToUpper(sha1Str) {
|
||||
result.Succeed = true
|
||||
result.Extra = efi
|
||||
fmt.Printf("[%s] %s 检测到同名文件,文件内容完全一致,无需重复上传: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.SavePath)
|
||||
utu.logf("[%s] %s 检测到同名文件,文件内容完全一致,无需重复上传: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.SavePath)
|
||||
return
|
||||
}
|
||||
// existed, delete it
|
||||
@@ -493,7 +546,7 @@ StepUploadPrepareUpload:
|
||||
return
|
||||
}
|
||||
time.Sleep(time.Duration(500) * time.Millisecond)
|
||||
fmt.Printf("[%s] %s 检测到同名文件,文件内容不一致,已将旧文件移动到回收站: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.SavePath)
|
||||
utu.logf("[%s] %s 检测到同名文件,文件内容不一致,已将旧文件移动到回收站: %s\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"), utu.SavePath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -544,7 +597,7 @@ StepUploadPrepareUpload:
|
||||
// 重试
|
||||
result.NeedRetry = true
|
||||
} else if apierr.Code == apierror.ApiCodeUploadPayloadTooLarge {
|
||||
fmt.Printf("[%s] %s 上传文件的大小超出限制,你可能需要开通阿里云盘三方权益包以便上传大文件\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
|
||||
utu.logf("[%s] %s 上传文件的大小超出限制,你可能需要开通阿里云盘三方权益包以便上传大文件\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -572,7 +625,7 @@ stepUploadUpload:
|
||||
if ee := utu.amendFileUploadPartNum(); ee != nil {
|
||||
// 修正分片乱序失败,先令上传任务直接失败
|
||||
logger.Verboseln("WARNING! amend uploaded parts num failed")
|
||||
fmt.Printf("[%s] %s 无法修正上传分片乱序的错误,建议重新上传\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
|
||||
utu.logf("[%s] %s 无法修正上传分片乱序的错误,建议重新上传\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
|
||||
uploadResult = &taskframework.TaskUnitRunResult{
|
||||
Succeed: false,
|
||||
NeedRetry: false,
|
||||
@@ -588,18 +641,34 @@ stepUploadUpload:
|
||||
}
|
||||
if errors.Is(uploadResult.Err, uploader.UploadNoSuchUpload) {
|
||||
// 上传任务过期
|
||||
fmt.Printf("[%s] %s 网盘上传任务不存在,创建新任务重新上传文件\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
|
||||
utu.logf("[%s] %s 网盘上传任务不存在,创建新任务重新上传文件\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
|
||||
// 需要重新从0开始上传
|
||||
uploadResult = nil
|
||||
utu.LocalFileChecksum.UploadOpEntity = nil
|
||||
utu.state = nil
|
||||
goto StepUploadPrepareUpload
|
||||
}
|
||||
if errors.Is(uploadResult.Err, uploader.UploadLocalFileAlreadyClosedError) {
|
||||
// 本地文件读取错误
|
||||
utu.logf("[%s] %s 本地文件读取错误,建议检查文件是否存在,是否有文件读取权限,或者硬盘是否有异常\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
|
||||
uploadResult = &taskframework.TaskUnitRunResult{
|
||||
Succeed: false,
|
||||
NeedRetry: false,
|
||||
Cancel: false,
|
||||
Err: uploadResult.Err,
|
||||
ResultCode: 0,
|
||||
ResultMessage: "",
|
||||
Extra: nil,
|
||||
}
|
||||
return uploadResult
|
||||
}
|
||||
|
||||
// 处理阿里云盘API接口错误
|
||||
var apier *apierror.ApiError
|
||||
if errors.As(uploadResult.Err, &apier) {
|
||||
// 上传任务过期
|
||||
if apier.Code == apierror.ApiCodeUploadIdNotFound {
|
||||
fmt.Printf("[%s] %s 网盘上传任务已失效,创建新任务重新上传文件\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
|
||||
utu.logf("[%s] %s 网盘上传任务已失效,创建新任务重新上传文件\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
|
||||
uploadResult = nil
|
||||
utu.LocalFileChecksum.UploadOpEntity = nil
|
||||
utu.state = nil
|
||||
|
||||
@@ -16,15 +16,15 @@ package panupload
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -35,7 +35,10 @@ const (
|
||||
// MaxRapidUploadSize 秒传文件支持的最大文件大小
|
||||
MaxRapidUploadSize = 20 * converter.GB
|
||||
|
||||
// UploadingFileName 上传文件上传状态的文件名
|
||||
UploadingFileName = "aliyunpan_uploading.json"
|
||||
// UploadingBackupFileName 上传文件上传状态的副本
|
||||
UploadingBackupFileName = "aliyunpan_uploading.json.bak"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -52,16 +55,16 @@ func getBlockSize(fileSize int64) int64 {
|
||||
|
||||
// IsUrlExpired 上传链接是否已过期。过期返回True
|
||||
func IsUrlExpired(urlStr string) bool {
|
||||
u, err := url.Parse(urlStr)
|
||||
expiredTimeSec, err := utils.GetExpiredTimeSecFromOSSUrl(urlStr)
|
||||
if err != nil {
|
||||
// 解析错误,默认过期
|
||||
return true
|
||||
}
|
||||
expiredTimeSecStr := u.Query().Get("x-oss-expires")
|
||||
expiredTimeSec, _ := strconv.ParseInt(expiredTimeSecStr, 10, 64)
|
||||
if (expiredTimeSec - time.Now().Unix()) <= 300 { // 小于5分钟
|
||||
// expired
|
||||
return true
|
||||
}
|
||||
// 返回未过期
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -84,7 +87,7 @@ func IsVideoFile(fileName string) bool {
|
||||
|
||||
// CalcFilePreHash 计算文件 PreHash
|
||||
func CalcFilePreHash(filePath string) string {
|
||||
localFile, _ := os.Open(filePath)
|
||||
localFile, _ := os.OpenFile(filePath, os.O_RDONLY, 0)
|
||||
defer localFile.Close()
|
||||
bytes := make([]byte, 1024)
|
||||
localFile.ReadAt(bytes, 0)
|
||||
|
||||
@@ -17,6 +17,16 @@ import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdliner"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdutil"
|
||||
@@ -27,19 +37,9 @@ import (
|
||||
"github.com/tickstep/library-go/cachepool"
|
||||
"github.com/tickstep/library-go/checkaccess"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/tickstep/library-go/getip"
|
||||
"github.com/tickstep/library-go/jsonhelper"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/library-go/requester"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -58,16 +58,16 @@ type tsResp struct {
|
||||
Msg string `json:"msg"`
|
||||
}
|
||||
|
||||
func getReleaseFromTicstep(client *requester.HTTPClient, showPrompt bool) *ReleaseInfo {
|
||||
func getReleaseFromTickstep(client *requester.HTTPClient, showPrompt bool) *ReleaseInfo {
|
||||
tsReleaseInfo := &ReleaseInfo{}
|
||||
tsRespObj := &tsResp{Data: tsReleaseInfo}
|
||||
fullUrl := strings.Builder{}
|
||||
ipAddr, err := getip.IPInfoFromTechainBaidu()
|
||||
ipAddr, err := utils.GetPublicIP()
|
||||
if err != nil {
|
||||
ipAddr = "127.0.0.1"
|
||||
}
|
||||
fmt.Fprintf(&fullUrl, "http://api.tickstep.com/update/tickstep/aliyunpan/releases/latest?ip=%s&os=%s&arch=%s&version=%s",
|
||||
ipAddr, runtime.GOOS, runtime.GOARCH, global.AppVersion)
|
||||
fmt.Fprintf(&fullUrl, "http://api.tickstep.com/update/tickstep/aliyunpan/releases/latest?ip=%s&os=%s&arch=%s&version=%s", ipAddr, runtime.GOOS, runtime.GOARCH, global.AppVersion)
|
||||
//fmt.Fprintf(&fullUrl, "http://localhost:8997/update/tickstep/aliyunpan/releases/latest?ip=%s&os=%s&arch=%s&version=%s", ipAddr, runtime.GOOS, runtime.GOARCH, global.AppVersion)
|
||||
resp, err := client.Req(http.MethodGet, fullUrl.String(), nil, nil)
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
@@ -136,19 +136,12 @@ func GetLatestReleaseInfo(showPrompt bool) *ReleaseInfo {
|
||||
client.SetTimeout(time.Duration(0) * time.Second)
|
||||
client.SetKeepAlive(true)
|
||||
|
||||
// check tickstep srv
|
||||
var tsReleaseInfo *ReleaseInfo = nil
|
||||
for idx := 0; idx < 3; idx++ {
|
||||
tsReleaseInfo = getReleaseFromTicstep(client, showPrompt)
|
||||
if tsReleaseInfo != nil {
|
||||
break
|
||||
}
|
||||
time.Sleep(time.Duration(5) * time.Second)
|
||||
}
|
||||
// tickstep server
|
||||
var tsReleaseInfo = getReleaseFromTickstep(client, showPrompt)
|
||||
|
||||
// github
|
||||
// github server
|
||||
var ghReleaseInfo *ReleaseInfo = nil
|
||||
for idx := 0; idx < 3; idx++ {
|
||||
for idx := 0; idx < 2; idx++ {
|
||||
ghReleaseInfo = getReleaseFromGithub(client, showPrompt)
|
||||
if ghReleaseInfo != nil {
|
||||
break
|
||||
@@ -158,10 +151,11 @@ func GetLatestReleaseInfo(showPrompt bool) *ReleaseInfo {
|
||||
|
||||
var releaseInfo *ReleaseInfo = nil
|
||||
if config.Config.UpdateCheckInfo.PreferUpdateSrv == "tickstep" {
|
||||
// theoretically, tickstep server will be more faster at mainland
|
||||
// theoretically, tickstep server will be faster in China mainland
|
||||
releaseInfo = tsReleaseInfo
|
||||
} else {
|
||||
releaseInfo = ghReleaseInfo
|
||||
// 如果github的版本信息获取不到(例如网络不稳定),可以尝试使用tickstep服务进行升级
|
||||
if ghReleaseInfo == nil || ghReleaseInfo.TagName == "" {
|
||||
releaseInfo = tsReleaseInfo
|
||||
}
|
||||
@@ -381,7 +375,8 @@ func CheckUpdate(version string, yes bool) {
|
||||
return
|
||||
}
|
||||
|
||||
execPath := cmdutil.ExecutablePath()
|
||||
//execPath := cmdutil.ExecutablePath()
|
||||
configPath := configurationPath()
|
||||
|
||||
var fileNum, errTimes int
|
||||
for _, zipFile := range reader.File {
|
||||
@@ -405,9 +400,11 @@ func CheckUpdate(version string, yes bool) {
|
||||
|
||||
name := zipFile.Name[strings.Index(zipFile.Name, "/")+1:]
|
||||
if name == ReleaseName {
|
||||
// 执行文件
|
||||
err = update(cmdutil.Executable(), rc)
|
||||
} else {
|
||||
err = update(filepath.Join(execPath, name), rc)
|
||||
// 配置文件
|
||||
err = update(filepath.Join(configPath, name), rc)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
@@ -424,3 +421,8 @@ func CheckUpdate(version string, yes bool) {
|
||||
|
||||
fmt.Printf("更新完毕, 请重启程序\n")
|
||||
}
|
||||
|
||||
// ConfigurationPath 获取程序配置所在目录
|
||||
func configurationPath() string {
|
||||
return config.GetConfigDir()
|
||||
}
|
||||
|
||||
@@ -44,10 +44,17 @@ func (p *IdlePlugin) SyncFileFinishCallback(context *Context, params *SyncFileFi
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *IdlePlugin) SyncAllFileFinishCallback(context *Context, params *SyncAllFileFinishParams) error {
|
||||
return nil
|
||||
}
|
||||
func (p *IdlePlugin) UserTokenRefreshFinishCallback(context *Context, params *UserTokenRefreshFinishParams) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *IdlePlugin) RemoveFilePrepareCallback(context *Context, params *RemoveFilePrepareParams) (*RemoveFilePrepareResult, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (p *IdlePlugin) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -31,6 +31,15 @@ func jsLog(call goja.FunctionCall) goja.Value {
|
||||
return str
|
||||
}
|
||||
|
||||
// jsLog 支持js中的console.log方法
|
||||
func jsPrintln(call goja.FunctionCall) goja.Value {
|
||||
str := call.Argument(0)
|
||||
buf := &strings.Builder{}
|
||||
fmt.Fprintf(buf, "[PLUGIN] %+v\n", str.Export())
|
||||
fmt.Println(buf.String())
|
||||
return str
|
||||
}
|
||||
|
||||
func (js *JsPlugin) Start() error {
|
||||
js.Name = "JsPlugin"
|
||||
js.vm = goja.New()
|
||||
@@ -38,8 +47,9 @@ func (js *JsPlugin) Start() error {
|
||||
|
||||
// 内置log
|
||||
console := js.vm.NewObject()
|
||||
console.Set("log", jsLog)
|
||||
js.vm.Set("console", console) // console.log()
|
||||
js.vm.Set("console", console)
|
||||
console.Set("log", jsLog) // console.log()
|
||||
console.Set("println", jsPrintln) // console.println()
|
||||
|
||||
// 内置系统函数PluginUtil
|
||||
pluginObj := js.vm.NewObject()
|
||||
@@ -56,6 +66,11 @@ func (js *JsPlugin) Start() error {
|
||||
pluginObj.Set("LocalFS", localFS)
|
||||
localFS.Set("deleteFile", DeleteLocalFile) // PluginUtil.LocalFS.deleteFile()
|
||||
|
||||
// PluginUtil.PanFS
|
||||
panFS := js.vm.NewObject()
|
||||
pluginObj.Set("PanFS", panFS)
|
||||
panFS.Set("deleteFile", DeletePanFile) // PluginUtil.PanFS.deleteFile()
|
||||
|
||||
// PluginUtil.Email
|
||||
emailObj := js.vm.NewObject()
|
||||
pluginObj.Set("Email", emailObj)
|
||||
@@ -69,6 +84,11 @@ func (js *JsPlugin) Start() error {
|
||||
kvObj.Set("getString", GetString) // PluginUtil.KV.getString()
|
||||
kvObj.Set("putString", PutString) // PluginUtil.KV.putString()
|
||||
|
||||
// PluginUtil.HashTool
|
||||
hashTool := js.vm.NewObject()
|
||||
pluginObj.Set("HashTool", hashTool)
|
||||
hashTool.Set("md5Hex", Md5Hex) // PluginUtil.HashTool.md5Hex()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -223,6 +243,26 @@ func (js *JsPlugin) SyncFileFinishCallback(context *Context, params *SyncFileFin
|
||||
return nil
|
||||
}
|
||||
|
||||
// SyncAllFileFinishCallback 同步备份-同步全部文件完成时的回调函数
|
||||
func (js *JsPlugin) SyncAllFileFinishCallback(context *Context, params *SyncAllFileFinishParams) error {
|
||||
var fn func(*Context, *SyncAllFileFinishParams) error
|
||||
if !js.isHandlerFuncExisted("syncAllFileFinishCallback") {
|
||||
return nil
|
||||
}
|
||||
err := js.vm.ExportTo(js.vm.Get("syncAllFileFinishCallback"), &fn)
|
||||
if err != nil {
|
||||
logger.Verboseln("Js函数映射到 Go 函数失败!")
|
||||
return nil
|
||||
}
|
||||
er := fn(context, params)
|
||||
if er != nil {
|
||||
logger.Verboseln(er)
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UserTokenRefreshFinishCallback 用户Token刷新完成后回调函数
|
||||
func (js *JsPlugin) UserTokenRefreshFinishCallback(context *Context, params *UserTokenRefreshFinishParams) error {
|
||||
var fn func(*Context, *UserTokenRefreshFinishParams) error
|
||||
if !js.isHandlerFuncExisted("userTokenRefreshFinishCallback") {
|
||||
@@ -241,6 +281,25 @@ func (js *JsPlugin) UserTokenRefreshFinishCallback(context *Context, params *Use
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveFilePrepareCallback 删除文件前的回调函数
|
||||
func (js *JsPlugin) RemoveFilePrepareCallback(context *Context, params *RemoveFilePrepareParams) (*RemoveFilePrepareResult, error) {
|
||||
var fn func(*Context, *RemoveFilePrepareParams) (*RemoveFilePrepareResult, error)
|
||||
if !js.isHandlerFuncExisted("removeFilePrepareCallback") {
|
||||
return nil, nil
|
||||
}
|
||||
err := js.vm.ExportTo(js.vm.Get("removeFilePrepareCallback"), &fn)
|
||||
if err != nil {
|
||||
logger.Verboseln("Js函数映射到 Go 函数失败!")
|
||||
return nil, nil
|
||||
}
|
||||
r, er := fn(context, params)
|
||||
if er != nil {
|
||||
logger.Verboseln(er)
|
||||
return nil, er
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (js *JsPlugin) Stop() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
+81
-11
@@ -4,7 +4,7 @@
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@@ -16,12 +16,16 @@ package plugins
|
||||
type (
|
||||
// Context 插件回调函数上下文信息
|
||||
Context struct {
|
||||
AppName string `json:"appName"`
|
||||
Version string `json:"version"`
|
||||
UserId string `json:"userId"`
|
||||
Nickname string `json:"nickname"`
|
||||
FileDriveId string `json:"fileDriveId"`
|
||||
AppName string `json:"appName"`
|
||||
Version string `json:"version"`
|
||||
UserId string `json:"userId"`
|
||||
Nickname string `json:"nickname"`
|
||||
// FileDriveId 备份盘
|
||||
FileDriveId string `json:"fileDriveId"`
|
||||
// AlbumDriveId 相册盘
|
||||
AlbumDriveId string `json:"albumDriveId"`
|
||||
// ResourceDriveId 资源盘
|
||||
ResourceDriveId string `json:"resourceDriveId"`
|
||||
}
|
||||
|
||||
// UploadFilePrepareParams 上传文件前的回调函数-参数
|
||||
@@ -66,6 +70,7 @@ type (
|
||||
DriveFileType string `json:"driveFileType"`
|
||||
DriveFileUpdatedAt string `json:"driveFileUpdatedAt"`
|
||||
LocalFilePath string `json:"localFilePath"`
|
||||
DownloadActionId string `json:"downloadActionId"`
|
||||
}
|
||||
|
||||
// DownloadFilePrepareResult 上传文件前的回调函数-返回结果
|
||||
@@ -79,6 +84,7 @@ type (
|
||||
// DownloadFileFinishParams 下载文件结束的回调函数-参数
|
||||
DownloadFileFinishParams struct {
|
||||
DriveId string `json:"driveId"`
|
||||
DriveFileId string `json:"driveFileId"`
|
||||
DriveFileName string `json:"driveFileName"`
|
||||
DriveFilePath string `json:"driveFilePath"`
|
||||
DriveFileSha1 string `json:"driveFileSha1"`
|
||||
@@ -87,6 +93,7 @@ type (
|
||||
DriveFileUpdatedAt string `json:"driveFileUpdatedAt"`
|
||||
DownloadResult string `json:"downloadResult"`
|
||||
LocalFilePath string `json:"localFilePath"`
|
||||
DownloadActionId string `json:"downloadActionId"`
|
||||
}
|
||||
|
||||
// SyncScanLocalFilePrepareParams 同步备份-扫描本地文件前参数
|
||||
@@ -122,11 +129,12 @@ type (
|
||||
SyncScanPanApproved string `json:"syncScanPanApproved"`
|
||||
}
|
||||
|
||||
// SyncFileFinishParams 同步备份-文件同步结束-回调参数
|
||||
// SyncFileFinishParams 同步备份-一个文件同步结束-回调参数
|
||||
SyncFileFinishParams struct {
|
||||
Action string `json:"action"`
|
||||
ActionResult string `json:"actionResult"`
|
||||
DriveId string `json:"driveId"`
|
||||
DriveFileId string `json:"driveFileId"`
|
||||
FileName string `json:"fileName"`
|
||||
FilePath string `json:"filePath"`
|
||||
FileSha1 string `json:"fileSha1"`
|
||||
@@ -135,6 +143,28 @@ type (
|
||||
FileUpdatedAt string `json:"fileUpdatedAt"`
|
||||
}
|
||||
|
||||
// SyncAllFileFinishParams 同步备份-全部文件同步结束-回调参数
|
||||
SyncAllFileFinishParams struct {
|
||||
// Name 任务名称
|
||||
Name string `json:"name"`
|
||||
// Id 任务ID
|
||||
Id string `json:"id"`
|
||||
// UserId 账号ID
|
||||
UserId string `json:"userId"`
|
||||
// DriveName 网盘名称,backup-备份盘,resource-资源盘
|
||||
DriveName string `json:"driveName"`
|
||||
// DriveId 网盘ID,目前支持文件网盘
|
||||
DriveId string `json:"driveId"`
|
||||
// LocalFolderPath 本地目录
|
||||
LocalFolderPath string `json:"localFolderPath"`
|
||||
// PanFolderPath 云盘目录
|
||||
PanFolderPath string `json:"panFolderPath"`
|
||||
// Mode 备份模式
|
||||
Mode string `json:"mode"`
|
||||
// Policy 备份策略
|
||||
Policy string `json:"policy"`
|
||||
}
|
||||
|
||||
// UserTokenRefreshFinishParams 用户Token刷新完成后回调函数
|
||||
UserTokenRefreshFinishParams struct {
|
||||
Result string `json:"result"`
|
||||
@@ -144,6 +174,42 @@ type (
|
||||
UpdatedAt string `json:"updatedAt"`
|
||||
}
|
||||
|
||||
// RemoveFilePrepareParams 删除文件前的回调函数-参数
|
||||
RemoveFilePrepareParams struct {
|
||||
Count int `json:"count"`
|
||||
Items []*RemoveFilePrepareItem `json:"items"`
|
||||
}
|
||||
RemoveFilePrepareItem struct {
|
||||
// DriveId 网盘ID
|
||||
DriveId string `json:"driveId"`
|
||||
// DriveFileId 网盘文件的ID
|
||||
DriveFileId string `json:"driveFileId"`
|
||||
// DriveFileName 网盘文件名
|
||||
DriveFileName string `json:"driveFileName"`
|
||||
// DriveFilePath 网盘文件路径
|
||||
DriveFilePath string `json:"driveFilePath"`
|
||||
// DriveFileSize 网盘文件大小
|
||||
DriveFileSize int64 `json:"driveFileSize"`
|
||||
// DriveFileType 网盘文件类型,file-文件,folder-文件夹
|
||||
DriveFileType string `json:"driveFileType"`
|
||||
// DriveFileUpdatedAt 网盘文件修改时间,格式:2025-03-03 10:39:14
|
||||
DriveFileUpdatedAt string `json:"driveFileUpdatedAt"`
|
||||
// DriveFileCreatedAt 网盘文件创建时间,格式:2025-03-03 10:39:14
|
||||
DriveFileCreatedAt string `json:"driveFileCreatedAt"`
|
||||
}
|
||||
// RemoveFilePrepareResult 删除文件前的回调函数-返回结果
|
||||
RemoveFilePrepareResult struct {
|
||||
Result []*RemoveFilePrepareResultItem `json:"result"`
|
||||
}
|
||||
RemoveFilePrepareResultItem struct {
|
||||
// DriveId 网盘ID
|
||||
DriveId string `json:"driveId"`
|
||||
// DriveFileId 网盘文件的ID
|
||||
DriveFileId string `json:"driveFileId"`
|
||||
// RemoveApproved 确认该文件是否删除。yes-删除 no-不删除
|
||||
RemoveApproved string `json:"removeApproved"`
|
||||
}
|
||||
|
||||
// Plugin 插件接口
|
||||
Plugin interface {
|
||||
// Start 启动
|
||||
@@ -164,18 +230,22 @@ type (
|
||||
// SyncScanLocalFilePrepareCallback 同步备份-扫描本地文件的回调函数
|
||||
SyncScanLocalFilePrepareCallback(context *Context, params *SyncScanLocalFilePrepareParams) (*SyncScanLocalFilePrepareResult, error)
|
||||
|
||||
// SyncScanPanFilePrepareCallback 同步备份-扫描本地文件的回调函数
|
||||
// SyncScanPanFilePrepareCallback 同步备份-扫描云盘文件的回调函数
|
||||
SyncScanPanFilePrepareCallback(context *Context, params *SyncScanPanFilePrepareParams) (*SyncScanPanFilePrepareResult, error)
|
||||
|
||||
// SyncFileFinishCallback 同步备份-同步文件完成的回调函数
|
||||
// SyncFileFinishCallback 同步备份-同步一个文件完成时的回调函数
|
||||
SyncFileFinishCallback(context *Context, params *SyncFileFinishParams) error
|
||||
|
||||
// SyncAllFileFinishCallback 同步备份-同步全部文件完成时的回调函数
|
||||
SyncAllFileFinishCallback(context *Context, params *SyncAllFileFinishParams) error
|
||||
|
||||
// UserTokenRefreshFinishCallback 用户Token刷新完成后回调函数
|
||||
UserTokenRefreshFinishCallback(context *Context, params *UserTokenRefreshFinishParams) error
|
||||
|
||||
// RemoveFilePrepareCallback 删除文件前的回调函数
|
||||
RemoveFilePrepareCallback(context *Context, params *RemoveFilePrepareParams) (*RemoveFilePrepareResult, error)
|
||||
|
||||
// Stop 停止
|
||||
Stop() error
|
||||
}
|
||||
)
|
||||
|
||||
var ()
|
||||
|
||||
@@ -30,12 +30,13 @@ func GetContext(user *config.PanUser) *Context {
|
||||
}
|
||||
}
|
||||
return &Context{
|
||||
AppName: "aliyunpan",
|
||||
Version: global.AppVersion,
|
||||
UserId: user.UserId,
|
||||
Nickname: user.Nickname,
|
||||
FileDriveId: user.DriveList.GetFileDriveId(),
|
||||
AlbumDriveId: user.DriveList.GetFileDriveId(),
|
||||
AppName: "aliyunpan",
|
||||
Version: global.AppVersion,
|
||||
UserId: user.UserId,
|
||||
Nickname: user.Nickname,
|
||||
FileDriveId: user.DriveList.GetFileDriveId(), // 备份盘
|
||||
AlbumDriveId: user.DriveList.GetAlbumDriveId(), // 相册盘,在OpenAPI接口中,这个相册盘ID是获取不到的
|
||||
ResourceDriveId: user.DriveList.GetResourceDriveId(), // 资源盘
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package plugins
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"github.com/jordan-wright/email"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/bolt"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/library-go/requester"
|
||||
@@ -62,6 +66,30 @@ func DeleteLocalFile(localFilePath string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// DeletePanFile 删除云盘文件,支持文件和文件夹
|
||||
func DeletePanFile(userId, driveId, panFileId string) bool {
|
||||
logger.Verboseln("[Plugin] try to delete pan file: userId=" + userId + ", driveId=" + driveId + ", panFileId=" + panFileId)
|
||||
if userId == "" || driveId == "" || panFileId == "" {
|
||||
return false
|
||||
}
|
||||
user := config.Config.UserList.GetUserByUserId(userId)
|
||||
if user == nil {
|
||||
logger.Verboseln("[Plugin] user not existed: ", userId)
|
||||
return false
|
||||
}
|
||||
fdr, err := user.PanClient().OpenapiPanClient().FileDelete(&aliyunpan.FileBatchActionParam{
|
||||
DriveId: driveId,
|
||||
FileId: panFileId,
|
||||
})
|
||||
if err != nil || !fdr.Success {
|
||||
logger.Verboseln("[Plugin] delete pan file error: ", err)
|
||||
return false
|
||||
} else {
|
||||
logger.Verboseln("[Plugin] delete pan file success: ", panFileId)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// SendTextMail 发送文本邮件
|
||||
func SendTextMail(mailServer, userName, password, to, subject, body string) bool {
|
||||
if e := sendEmail(mailServer, userName, password, to, subject, body, "text", true); e != nil {
|
||||
@@ -166,3 +194,9 @@ func PutString(key, value string) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Md5Hex md5加密
|
||||
func Md5Hex(text string) string {
|
||||
hash := md5.Sum([]byte(text))
|
||||
return fmt.Sprintf("%x", hash[:])
|
||||
}
|
||||
|
||||
@@ -528,6 +528,15 @@ func (f *FileActionTask) uploadFile(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
if len(f.syncItem.UploadEntity.PartInfoList) == 0 {
|
||||
// finished
|
||||
f.syncItem.Status = SyncFileStatusSuccess
|
||||
f.syncItem.StatusUpdateTime = utils.NowTimeStr()
|
||||
f.syncFileDb.Update(f.syncItem)
|
||||
|
||||
PromptPrintln("上传完毕:" + f.syncItem.getPanFileFullPath())
|
||||
return nil
|
||||
}
|
||||
// 检测链接是否过期
|
||||
// check url expired or not
|
||||
uploadUrl := f.syncItem.UploadEntity.PartInfoList[f.syncItem.UploadPartSeq].UploadURL
|
||||
|
||||
@@ -271,7 +271,6 @@ func (f *FileActionTaskManager) doFileDiffRoutine(localFiles LocalFileList, panF
|
||||
// 本地文件和云盘文件SHA1不一样
|
||||
// 不同模式同步策略不一样
|
||||
if f.task.Mode == Upload {
|
||||
|
||||
// 不再这里计算SHA1,待到上传的时候再计算
|
||||
//if localFile.Sha1Hash == "" {
|
||||
// // 计算本地文件SHA1
|
||||
@@ -630,6 +629,7 @@ func (f *FileActionTaskManager) doPluginCallback(syncFile *SyncFileItem, actionR
|
||||
Action: string(syncFile.Action),
|
||||
ActionResult: actionResult,
|
||||
DriveId: syncFile.DriveId,
|
||||
DriveFileId: "",
|
||||
FileName: file.FileName,
|
||||
FilePath: syncFile.getPanFileFullPath(),
|
||||
FileSha1: file.Sha1Hash,
|
||||
@@ -645,6 +645,7 @@ func (f *FileActionTaskManager) doPluginCallback(syncFile *SyncFileItem, actionR
|
||||
Action: string(syncFile.Action),
|
||||
ActionResult: actionResult,
|
||||
DriveId: syncFile.DriveId,
|
||||
DriveFileId: file.FileId,
|
||||
FileName: file.FileName,
|
||||
FilePath: syncFile.getLocalFileFullPath(),
|
||||
FileSha1: file.Sha1Hash,
|
||||
|
||||
@@ -803,3 +803,24 @@ func (t *SyncTask) scanPanFile(ctx context.Context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// doAllFileSyncPluginCallback 全部文件同步完成回调方法
|
||||
func (t *SyncTask) doAllFileSyncPluginCallback() {
|
||||
// 插件回调
|
||||
pluginParam := &plugins.SyncAllFileFinishParams{
|
||||
Name: t.Name,
|
||||
Id: t.Id,
|
||||
UserId: t.UserId,
|
||||
DriveName: t.DriveName,
|
||||
DriveId: t.DriveId,
|
||||
LocalFolderPath: t.LocalFolderPath,
|
||||
PanFolderPath: t.PanFolderPath,
|
||||
Mode: string(t.Mode),
|
||||
Policy: string(t.Policy),
|
||||
}
|
||||
t.pluginMutex.Lock()
|
||||
defer t.pluginMutex.Unlock()
|
||||
if er := t.plugin.SyncAllFileFinishCallback(plugins.GetContext(t.panUser), pluginParam); er == nil {
|
||||
logger.Verboseln("成功调用同步任务完成插件回调方法:" + t.Id)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -235,3 +235,12 @@ func (m *SyncTaskManager) IsAllTaskCompletely() bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// DoTaskSyncCompletelyPluginCallback 调用任务同步完成的回调函数
|
||||
func (m *SyncTaskManager) DoTaskSyncCompletelyPluginCallback() {
|
||||
for _, task := range m.syncDriveConfig.SyncTaskList {
|
||||
if task.IsTaskCompletely() {
|
||||
task.doAllFileSyncPluginCallback()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,833 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/tickstep/library-go/requester/rio/speeds"
|
||||
)
|
||||
|
||||
type TaskState int
|
||||
type DashboardType int
|
||||
|
||||
const (
|
||||
TaskQueued TaskState = iota
|
||||
TaskRunning
|
||||
TaskSuccess
|
||||
TaskFailed
|
||||
TaskSkipped
|
||||
TaskCanceled
|
||||
|
||||
// DashboardPanelDownload 下载面板类型
|
||||
DashboardPanelDownload DashboardType = 1
|
||||
// DashboardPanelUpload 上传面板类型
|
||||
DashboardPanelUpload DashboardType = 2
|
||||
)
|
||||
|
||||
type DashboardOptions struct {
|
||||
Title string
|
||||
MaxHistory int
|
||||
ActiveSlots int
|
||||
Output io.Writer
|
||||
}
|
||||
|
||||
// DashboardPanel 统计面板使用了很多ANSI转义序列。并不是所有的终端都支持ANSI转义序列,所以需要做兼容性处理。
|
||||
type DashboardPanel struct {
|
||||
dashboardType DashboardType
|
||||
mu sync.Mutex
|
||||
tasks map[string]*dashboardTask
|
||||
order []string
|
||||
history []string
|
||||
maxHistory int
|
||||
parallel int
|
||||
activeSlots int
|
||||
globalSpeeds *speeds.Speeds
|
||||
title string
|
||||
startTime time.Time
|
||||
renderEvery time.Duration
|
||||
stopCh chan struct{}
|
||||
doneCh chan struct{}
|
||||
startOnce sync.Once
|
||||
closeOnce sync.Once
|
||||
out io.Writer
|
||||
}
|
||||
|
||||
type dashboardTask struct {
|
||||
id string
|
||||
path string
|
||||
name string
|
||||
total int64
|
||||
downloaded int64
|
||||
speed int64
|
||||
eta time.Duration
|
||||
state TaskState
|
||||
message string
|
||||
isFile bool
|
||||
}
|
||||
|
||||
// NewDashboardPanel 创建下载任务统计面板
|
||||
func NewDashboardPanel(dashboardType DashboardType, parallel int, globalSpeeds *speeds.Speeds, opts *DashboardOptions) *DashboardPanel {
|
||||
if parallel < 1 {
|
||||
parallel = 1
|
||||
}
|
||||
db := &DashboardPanel{
|
||||
dashboardType: dashboardType,
|
||||
tasks: make(map[string]*dashboardTask),
|
||||
order: make([]string, 0, 128),
|
||||
history: make([]string, 0, 64),
|
||||
maxHistory: 8,
|
||||
parallel: parallel,
|
||||
activeSlots: 5,
|
||||
globalSpeeds: globalSpeeds,
|
||||
title: "aliyunpan统计面板",
|
||||
renderEvery: 500 * time.Millisecond,
|
||||
stopCh: make(chan struct{}),
|
||||
doneCh: make(chan struct{}),
|
||||
out: os.Stdout,
|
||||
}
|
||||
if opts != nil {
|
||||
if opts.Title != "" {
|
||||
db.title = opts.Title
|
||||
}
|
||||
if opts.MaxHistory > 0 {
|
||||
db.maxHistory = opts.MaxHistory
|
||||
}
|
||||
if opts.ActiveSlots > 0 {
|
||||
db.activeSlots = opts.ActiveSlots
|
||||
}
|
||||
if opts.Output != nil {
|
||||
db.out = opts.Output
|
||||
}
|
||||
}
|
||||
if db.activeSlots < 1 {
|
||||
db.activeSlots = 1
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
// Start 启动面板
|
||||
func (db *DashboardPanel) Start() {
|
||||
if db == nil {
|
||||
return
|
||||
}
|
||||
db.startOnce.Do(func() {
|
||||
db.startTime = time.Now()
|
||||
enableVirtualTerminalProcessing()
|
||||
fmt.Fprint(db.out, "\x1b[?25l") // 隐藏光标
|
||||
db.render()
|
||||
ticker := time.NewTicker(db.renderEvery)
|
||||
go func() {
|
||||
defer close(db.doneCh)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-db.stopCh:
|
||||
// 延迟1秒
|
||||
time.Sleep(1 * time.Second)
|
||||
// 渲染
|
||||
db.render()
|
||||
return
|
||||
case <-ticker.C:
|
||||
db.render()
|
||||
}
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
||||
// Close 关闭面板
|
||||
func (db *DashboardPanel) Close() {
|
||||
if db == nil {
|
||||
return
|
||||
}
|
||||
db.closeOnce.Do(func() {
|
||||
close(db.stopCh)
|
||||
<-db.doneCh
|
||||
fmt.Fprint(db.out, "\x1b[?25h") // 显示光标
|
||||
})
|
||||
}
|
||||
|
||||
// RegisterTask 注册任务信息
|
||||
func (db *DashboardPanel) RegisterTask(id, path string, total int64, isFile bool) {
|
||||
if db == nil || id == "" {
|
||||
return
|
||||
}
|
||||
db.mu.Lock()
|
||||
defer db.mu.Unlock()
|
||||
task := db.getOrCreateTaskLocked(id)
|
||||
if path != "" {
|
||||
task.path = path
|
||||
}
|
||||
if task.name == "" {
|
||||
task.name = baseName(path)
|
||||
}
|
||||
if total > 0 {
|
||||
task.total = total
|
||||
}
|
||||
task.isFile = isFile
|
||||
}
|
||||
|
||||
// UpdateTaskInfo 更新任务信息
|
||||
func (db *DashboardPanel) UpdateTaskInfo(id, name string, total int64, isFile bool) {
|
||||
if db == nil || id == "" {
|
||||
return
|
||||
}
|
||||
db.mu.Lock()
|
||||
defer db.mu.Unlock()
|
||||
task := db.getOrCreateTaskLocked(id)
|
||||
if name != "" {
|
||||
task.name = name
|
||||
}
|
||||
if total > 0 {
|
||||
task.total = total
|
||||
}
|
||||
task.isFile = isFile
|
||||
}
|
||||
|
||||
// UpdateTaskProgress 更新任务进度
|
||||
func (db *DashboardPanel) UpdateTaskProgress(id string, downloaded, total, speed int64, eta time.Duration) {
|
||||
if db == nil || id == "" {
|
||||
return
|
||||
}
|
||||
db.mu.Lock()
|
||||
defer db.mu.Unlock()
|
||||
task := db.getOrCreateTaskLocked(id)
|
||||
task.downloaded = downloaded
|
||||
if total > 0 {
|
||||
task.total = total
|
||||
}
|
||||
task.speed = speed
|
||||
task.eta = eta
|
||||
}
|
||||
|
||||
// MarkTaskState 标记任务状态
|
||||
func (db *DashboardPanel) MarkTaskState(id string, state TaskState, message string) {
|
||||
if db == nil || id == "" {
|
||||
return
|
||||
}
|
||||
db.mu.Lock()
|
||||
defer db.mu.Unlock()
|
||||
task := db.getOrCreateTaskLocked(id)
|
||||
task.state = state
|
||||
if message != "" {
|
||||
task.message = message
|
||||
}
|
||||
switch state {
|
||||
case TaskSuccess, TaskFailed, TaskSkipped, TaskCanceled:
|
||||
db.appendHistoryLocked(db.defaultHistoryMessage(task, message))
|
||||
}
|
||||
}
|
||||
|
||||
// Logf 增加日志
|
||||
func (db *DashboardPanel) Logf(format string, a ...interface{}) {
|
||||
if db == nil {
|
||||
return
|
||||
}
|
||||
msg := fmt.Sprintf(format, a...)
|
||||
msg = strings.TrimRight(msg, "\r\n")
|
||||
if msg == "" {
|
||||
return
|
||||
}
|
||||
// 去掉开头的换行符
|
||||
msg = strings.TrimPrefix(msg, "\n")
|
||||
// 去掉回车换行符,UI面板的日志只能单行显示
|
||||
msg = strings.ReplaceAll(msg, "\n", " ")
|
||||
db.mu.Lock()
|
||||
defer db.mu.Unlock()
|
||||
db.appendHistoryLocked(msg)
|
||||
}
|
||||
|
||||
func (db *DashboardPanel) getOrCreateTaskLocked(id string) *dashboardTask {
|
||||
if task, ok := db.tasks[id]; ok {
|
||||
return task
|
||||
}
|
||||
task := &dashboardTask{
|
||||
id: id,
|
||||
state: TaskQueued,
|
||||
}
|
||||
db.tasks[id] = task
|
||||
db.order = append(db.order, id)
|
||||
return task
|
||||
}
|
||||
|
||||
func (db *DashboardPanel) appendHistoryLocked(message string) {
|
||||
if message == "" {
|
||||
return
|
||||
}
|
||||
timestamp := time.Now().Format("15:04:05")
|
||||
db.history = append(db.history, fmt.Sprintf("[%s] %s", timestamp, message))
|
||||
if len(db.history) > db.maxHistory*3 {
|
||||
db.history = db.history[len(db.history)-db.maxHistory*2:]
|
||||
}
|
||||
}
|
||||
|
||||
func (db *DashboardPanel) defaultHistoryMessage(task *dashboardTask, message string) string {
|
||||
if message != "" {
|
||||
return message
|
||||
}
|
||||
name := task.path
|
||||
if name == "" {
|
||||
name = task.name
|
||||
}
|
||||
switch task.state {
|
||||
case TaskSuccess:
|
||||
return "完成: " + name
|
||||
case TaskFailed:
|
||||
return "失败: " + name
|
||||
case TaskSkipped:
|
||||
return "跳过: " + name
|
||||
case TaskCanceled:
|
||||
return "取消: " + name
|
||||
default:
|
||||
return name
|
||||
}
|
||||
}
|
||||
|
||||
// render 渲染面板
|
||||
func (db *DashboardPanel) render() {
|
||||
if db == nil {
|
||||
return
|
||||
}
|
||||
width, height := terminalSize()
|
||||
if width < 60 {
|
||||
width = 60
|
||||
}
|
||||
innerWidth := width - 2
|
||||
snapshot := db.snapshot()
|
||||
|
||||
// 统计总览
|
||||
headerLines := db.headerLines(snapshot, innerWidth)
|
||||
|
||||
// 下载任务队列详情
|
||||
//activeTitle := db.activeTitle(snapshot)
|
||||
activeLines := db.activeLines(snapshot, innerWidth)
|
||||
|
||||
// 下载日志
|
||||
headerHeight := len(headerLines) + 2
|
||||
activeHeight := len(activeLines) + 1
|
||||
historyBase := 3
|
||||
minHistory := 1
|
||||
minHeight := headerHeight + activeHeight + historyBase + minHistory
|
||||
if height < minHeight {
|
||||
height = minHeight
|
||||
}
|
||||
historyLines := height - (headerHeight + activeHeight + historyBase)
|
||||
if historyLines < minHistory {
|
||||
historyLines = minHistory
|
||||
}
|
||||
historyBody := db.historyLines(snapshot, innerWidth, historyLines)
|
||||
|
||||
// 拼接并格式化输出
|
||||
builder := &strings.Builder{}
|
||||
builder.WriteString(renderBox(headerLines, width))
|
||||
builder.WriteString(renderTitledBox("ActiveDownloads", activeLines, width))
|
||||
builder.WriteString(renderTitledBox("History", historyBody, width))
|
||||
content := builder.String()
|
||||
|
||||
// 使用双缓冲技术
|
||||
var buf strings.Builder
|
||||
// 构建完整内容
|
||||
buf.WriteString("\x1b[H\x1b[2J") // 清屏操作
|
||||
buf.WriteString(content)
|
||||
// 一次性输出
|
||||
fmt.Fprint(db.out, buf.String())
|
||||
}
|
||||
|
||||
type dashboardSnapshot struct {
|
||||
tasks []*dashboardTask
|
||||
history []string
|
||||
}
|
||||
|
||||
func (db *DashboardPanel) snapshot() dashboardSnapshot {
|
||||
db.mu.Lock()
|
||||
defer db.mu.Unlock()
|
||||
tasks := make([]*dashboardTask, 0, len(db.order))
|
||||
for _, id := range db.order {
|
||||
if task, ok := db.tasks[id]; ok {
|
||||
copied := *task
|
||||
tasks = append(tasks, &copied)
|
||||
}
|
||||
}
|
||||
history := append([]string(nil), db.history...)
|
||||
return dashboardSnapshot{tasks: tasks, history: history}
|
||||
}
|
||||
|
||||
func renderBox(lines []string, width int) string {
|
||||
if width < 4 {
|
||||
width = 4
|
||||
}
|
||||
innerWidth := width - 2
|
||||
builder := &strings.Builder{}
|
||||
builder.WriteString("\u250c")
|
||||
builder.WriteString(strings.Repeat("\u2500", innerWidth))
|
||||
builder.WriteString("\u2510")
|
||||
for _, line := range lines {
|
||||
builder.WriteString("\n")
|
||||
builder.WriteString("\u2502") // 左边框
|
||||
builder.WriteString(fitLine(line, innerWidth))
|
||||
builder.WriteString("\u2502") // 右边框
|
||||
}
|
||||
builder.WriteString("\n")
|
||||
builder.WriteString("\u2502") // 左边框
|
||||
//builder.WriteString("\u2514") // 左下角
|
||||
builder.WriteString(strings.Repeat("\u2500", innerWidth))
|
||||
builder.WriteString("\u2502") // 右边框
|
||||
//builder.WriteString("\u2518") // 右下角
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func renderTitledBox(title string, body []string, width int) string {
|
||||
if width < 4 {
|
||||
width = 4
|
||||
}
|
||||
innerWidth := width - 2
|
||||
builder := &strings.Builder{}
|
||||
|
||||
for _, line := range body {
|
||||
builder.WriteString("\n")
|
||||
builder.WriteString("\u2502") // 左边框
|
||||
builder.WriteString(fitLine(line, innerWidth)) // 内容,使用空格补足宽度
|
||||
builder.WriteString("\u2502") // 右边框
|
||||
}
|
||||
|
||||
builder.WriteString("\n")
|
||||
if strings.HasPrefix(title, "History") {
|
||||
builder.WriteString("\u2514") // 左下角
|
||||
} else {
|
||||
builder.WriteString("\u2502") // 左边框
|
||||
}
|
||||
builder.WriteString(strings.Repeat("\u2500", innerWidth)) // 表格横线
|
||||
if strings.HasPrefix(title, "History") {
|
||||
builder.WriteString("\u2518") // 右下角
|
||||
} else {
|
||||
builder.WriteString("\u2502") // 右边框
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func (db *DashboardPanel) headerLines(snapshot dashboardSnapshot, innerWidth int) []string {
|
||||
totalFiles, doneFiles, failedFiles := db.fileCounts(snapshot.tasks)
|
||||
downloaded, total := db.totalProgress(snapshot.tasks)
|
||||
speed := db.totalSpeed(snapshot.tasks)
|
||||
elapsed := time.Since(db.startTime)
|
||||
left := "-"
|
||||
if speed > 0 && total > downloaded {
|
||||
left = formatDurationShort(time.Duration((total-downloaded)/speed) * time.Second)
|
||||
}
|
||||
|
||||
status := "进行中"
|
||||
if db.dashboardType == DashboardPanelDownload {
|
||||
status = "下载中"
|
||||
} else if db.dashboardType == DashboardPanelUpload {
|
||||
status = "上传中"
|
||||
}
|
||||
if totalFiles > 0 && doneFiles >= totalFiles {
|
||||
if failedFiles > 0 {
|
||||
status = "已完成 (有错误)"
|
||||
} else {
|
||||
status = "已完成"
|
||||
}
|
||||
}
|
||||
|
||||
line1 := fmt.Sprintf("总速度: %s | 文件: %d/%d | 失败: %d | 状态: %s | 已用时间: %s | 剩余时间: %s",
|
||||
utils.FormatSpeedFixedWidth(speed, 11), doneFiles, totalFiles, failedFiles, status, formatDurationShort(elapsed), left)
|
||||
line1 = fitLine(line1, innerWidth)
|
||||
|
||||
progressPct := 0.0
|
||||
if total > 0 {
|
||||
progressPct = float64(downloaded) / float64(total)
|
||||
}
|
||||
progress := fmt.Sprintf("%s/%s", converter.ConvertFileSize(downloaded, 2), converter.ConvertFileSize(total, 2))
|
||||
label := "总进度: "
|
||||
suffix := fmt.Sprintf(" %5.1f%% (%s)", progressPct*100, progress)
|
||||
barWidth := innerWidth - displayWidth(label) - displayWidth(suffix) - 2
|
||||
if barWidth < 10 {
|
||||
barWidth = 10
|
||||
}
|
||||
bar := renderBar(progressPct, barWidth)
|
||||
line2 := fmt.Sprintf("%s[%s]%s", label, bar, suffix)
|
||||
line2 = fitLine(line2, innerWidth)
|
||||
|
||||
return []string{line1, line2}
|
||||
}
|
||||
|
||||
func (db *DashboardPanel) activeTitle(snapshot dashboardSnapshot) string {
|
||||
running := 0
|
||||
queued := 0
|
||||
for _, task := range snapshot.tasks {
|
||||
if !task.isFile {
|
||||
continue
|
||||
}
|
||||
switch task.state {
|
||||
case TaskRunning:
|
||||
running++
|
||||
case TaskQueued:
|
||||
queued++
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("Active Downloads (slots %d, running %d, queued %d)", db.activeSlots, running, queued)
|
||||
}
|
||||
|
||||
func (db *DashboardPanel) activeLines(snapshot dashboardSnapshot, innerWidth int) []string {
|
||||
active := db.pickActiveTasks(snapshot.tasks)
|
||||
lines := make([]string, 0, db.activeSlots)
|
||||
for i := 0; i < db.activeSlots; i++ {
|
||||
if i < len(active) {
|
||||
lines = append(lines, renderTaskLine(active[i], i, innerWidth))
|
||||
continue
|
||||
}
|
||||
placeholder := &dashboardTask{name: "[空闲...]", state: TaskQueued}
|
||||
lines = append(lines, renderTaskLine(placeholder, i, innerWidth))
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
func (db *DashboardPanel) historyLines(snapshot dashboardSnapshot, innerWidth int, lines int) []string {
|
||||
if lines < 1 {
|
||||
lines = 1
|
||||
}
|
||||
maxLines := lines
|
||||
if db.maxHistory > 0 && maxLines > db.maxHistory {
|
||||
maxLines = db.maxHistory
|
||||
}
|
||||
if maxLines > len(snapshot.history) {
|
||||
maxLines = len(snapshot.history)
|
||||
}
|
||||
result := make([]string, 0, lines)
|
||||
start := len(snapshot.history) - maxLines
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
for i := len(snapshot.history) - 1; i >= start && len(result) < maxLines; i-- {
|
||||
result = append(result, truncateText(snapshot.history[i], innerWidth))
|
||||
}
|
||||
if len(result) == 0 {
|
||||
result = append(result, truncateText("No recent events.", innerWidth))
|
||||
}
|
||||
|
||||
// 倒序数组
|
||||
reversedResult := make([]string, 0, len(result))
|
||||
for i := len(result) - 1; i >= 0; i-- {
|
||||
reversedResult = append(reversedResult, result[i])
|
||||
}
|
||||
result = reversedResult
|
||||
|
||||
// 补全空行
|
||||
for len(result) < lines {
|
||||
result = append(result, "")
|
||||
}
|
||||
return result[:lines]
|
||||
}
|
||||
|
||||
func (db *DashboardPanel) fileCounts(tasks []*dashboardTask) (total int, done int, failed int) {
|
||||
for _, task := range tasks {
|
||||
if !task.isFile {
|
||||
continue
|
||||
}
|
||||
total++
|
||||
switch task.state {
|
||||
case TaskSuccess, TaskSkipped, TaskFailed, TaskCanceled:
|
||||
done++
|
||||
}
|
||||
if task.state == TaskFailed {
|
||||
failed++
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DashboardPanel) totalProgress(tasks []*dashboardTask) (downloaded int64, total int64) {
|
||||
for _, task := range tasks {
|
||||
if !task.isFile {
|
||||
continue
|
||||
}
|
||||
if task.total > 0 {
|
||||
total += task.total
|
||||
if task.state == TaskSuccess || task.state == TaskSkipped {
|
||||
// 成功、跳过的文件,统计上当做已经下载完成
|
||||
downloaded += task.total
|
||||
} else {
|
||||
if task.downloaded >= task.total {
|
||||
downloaded += task.total
|
||||
} else {
|
||||
downloaded += task.downloaded
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DashboardPanel) totalSpeed(tasks []*dashboardTask) int64 {
|
||||
if db.globalSpeeds != nil {
|
||||
return db.globalSpeeds.GetSpeeds()
|
||||
}
|
||||
var sum int64
|
||||
for _, task := range tasks {
|
||||
sum += task.speed
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
func (db *DashboardPanel) pickActiveTasks(tasks []*dashboardTask) []*dashboardTask {
|
||||
slotCap := db.activeSlots
|
||||
if slotCap < 1 {
|
||||
slotCap = 1
|
||||
}
|
||||
active := make([]*dashboardTask, 0, slotCap)
|
||||
queued := make([]*dashboardTask, 0, slotCap)
|
||||
for _, task := range tasks {
|
||||
if !task.isFile {
|
||||
continue
|
||||
}
|
||||
switch task.state {
|
||||
case TaskRunning:
|
||||
active = append(active, task)
|
||||
case TaskQueued:
|
||||
queued = append(queued, task)
|
||||
}
|
||||
}
|
||||
active = append(active, queued...)
|
||||
if len(active) > slotCap {
|
||||
active = active[:slotCap]
|
||||
}
|
||||
return active
|
||||
}
|
||||
|
||||
func renderTaskLine(task *dashboardTask, index int, width int) string {
|
||||
if width < 20 {
|
||||
width = 20
|
||||
}
|
||||
prefix := fmt.Sprintf("%2d. ", index+1)
|
||||
name := task.name
|
||||
if name == "" {
|
||||
name = baseName(task.path)
|
||||
}
|
||||
|
||||
// 百分比
|
||||
progressPct := 0.0
|
||||
percent := "--.-%"
|
||||
if task.total > 0 {
|
||||
progressPct = float64(task.downloaded) / float64(task.total)
|
||||
percent = fmt.Sprintf("%5.1f%%", math.Min(100, progressPct*100)) // 将浮点数格式化为:总宽度 5 个字符,包含 1 位小数,后面加百分号
|
||||
} else {
|
||||
// 空闲队列左边多一个空格,对齐活跃队列
|
||||
percent = " --.-%"
|
||||
}
|
||||
|
||||
// 速度
|
||||
rate := "-"
|
||||
// 剩余时间
|
||||
eta := "-"
|
||||
switch task.state {
|
||||
case TaskQueued:
|
||||
rate = "等待中"
|
||||
case TaskRunning:
|
||||
if task.speed > 0 {
|
||||
rate = converter.ConvertFileSize(task.speed, 2) + "/s"
|
||||
} else {
|
||||
rate = "0 B/s"
|
||||
}
|
||||
if task.eta > 0 {
|
||||
eta = formatDurationShort(task.eta)
|
||||
}
|
||||
case TaskSuccess:
|
||||
rate = "done"
|
||||
case TaskFailed:
|
||||
rate = "failed"
|
||||
case TaskSkipped:
|
||||
rate = "skipped"
|
||||
case TaskCanceled:
|
||||
rate = "canceled"
|
||||
}
|
||||
|
||||
barWidth := 20
|
||||
rateWidth := 12
|
||||
etaWidth := 8
|
||||
percentWidth := displayWidth(percent)
|
||||
fixed := displayWidth(prefix) + 1 + 2 /*进度条左右[]括号*/ + barWidth + 1 + percentWidth + 1 + rateWidth + 1 + etaWidth
|
||||
nameWidth := width - fixed
|
||||
if nameWidth < 8 {
|
||||
reduce := 8 - nameWidth
|
||||
if barWidth > 10 {
|
||||
cut := minInt(reduce, barWidth-10)
|
||||
barWidth -= cut
|
||||
reduce -= cut
|
||||
}
|
||||
if rateWidth > 8 && reduce > 0 {
|
||||
cut := minInt(reduce, rateWidth-8)
|
||||
rateWidth -= cut
|
||||
reduce -= cut
|
||||
}
|
||||
if etaWidth > 5 && reduce > 0 {
|
||||
cut := minInt(reduce, etaWidth-5)
|
||||
etaWidth -= cut
|
||||
reduce -= cut
|
||||
}
|
||||
fixed = displayWidth(prefix) + 1 /*1个空格分隔*/ + 2 /*进度条左右[]括号*/ + barWidth /*进度条长度*/ + 1 /*1个空格分隔*/ + percentWidth + 1 /*1个空格分隔*/ + rateWidth + 1 /*1个空格分隔*/ + etaWidth
|
||||
nameWidth = width - fixed
|
||||
if nameWidth < 4 {
|
||||
nameWidth = 4
|
||||
}
|
||||
}
|
||||
|
||||
name = padRight(truncateText(name, nameWidth), nameWidth)
|
||||
bar := renderBar(progressPct, barWidth)
|
||||
rate = padRight(truncateText(rate, rateWidth), rateWidth)
|
||||
eta = padRight(truncateText(eta, etaWidth), etaWidth)
|
||||
line := fmt.Sprintf("%s%s [%s] %s %s %s", prefix, name, bar, percent, rate, eta)
|
||||
return fitLine(line, width)
|
||||
}
|
||||
|
||||
// renderBar 绘制进度条
|
||||
func renderBar(pct float64, width int) string {
|
||||
if width < 5 {
|
||||
width = 5
|
||||
}
|
||||
if pct < 0 {
|
||||
pct = 0
|
||||
}
|
||||
if pct > 1 {
|
||||
pct = 1
|
||||
}
|
||||
filled := int(math.Floor(pct * float64(width)))
|
||||
// 已经下载的部分使用 => 表示,没有下载的部分使用 空格 表示
|
||||
if filled >= 1 {
|
||||
return strings.Repeat("=", filled-1) + ">" + strings.Repeat(" ", width-filled)
|
||||
}
|
||||
return strings.Repeat(" ", width-filled)
|
||||
}
|
||||
|
||||
func formatDurationShort(d time.Duration) string {
|
||||
if d <= 0 {
|
||||
return "-"
|
||||
}
|
||||
seconds := int(d.Seconds())
|
||||
if seconds < 60 {
|
||||
return fmt.Sprintf("%ds", seconds)
|
||||
}
|
||||
minutes := seconds / 60
|
||||
seconds = seconds % 60
|
||||
if minutes < 60 {
|
||||
return fmt.Sprintf("%dm%02ds", minutes, seconds)
|
||||
}
|
||||
hours := minutes / 60
|
||||
minutes = minutes % 60
|
||||
if hours < 100 {
|
||||
return fmt.Sprintf("%dh%02dm", hours, minutes)
|
||||
}
|
||||
return fmt.Sprintf("%dh", hours)
|
||||
}
|
||||
|
||||
func truncateText(text string, max int) string {
|
||||
if max <= 0 || text == "" {
|
||||
return ""
|
||||
}
|
||||
if displayWidth(text) <= max {
|
||||
return text
|
||||
}
|
||||
if max <= 3 {
|
||||
return takeWidth(text, max)
|
||||
}
|
||||
return takeWidth(text, max-3) + "..."
|
||||
}
|
||||
|
||||
func padRight(text string, width int) string {
|
||||
if width <= 0 {
|
||||
return text
|
||||
}
|
||||
if displayWidth(text) >= width {
|
||||
return text
|
||||
}
|
||||
return text + strings.Repeat(" ", width-displayWidth(text))
|
||||
}
|
||||
|
||||
func fitLine(text string, width int) string {
|
||||
return padRight(truncateText(text, width), width)
|
||||
}
|
||||
|
||||
func displayWidth(text string) int {
|
||||
width := 0
|
||||
for _, r := range text {
|
||||
width += runeWidth(r)
|
||||
}
|
||||
return width
|
||||
}
|
||||
|
||||
func takeWidth(text string, max int) string {
|
||||
if max <= 0 {
|
||||
return ""
|
||||
}
|
||||
used := 0
|
||||
for idx, r := range text {
|
||||
w := runeWidth(r)
|
||||
if used+w > max {
|
||||
return text[:idx]
|
||||
}
|
||||
used += w
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func runeWidth(r rune) int {
|
||||
if r == 0 {
|
||||
return 0
|
||||
}
|
||||
if r < 0x1100 {
|
||||
return 1
|
||||
}
|
||||
switch {
|
||||
case r >= 0x1100 && r <= 0x115f,
|
||||
r == 0x2329 || r == 0x232a,
|
||||
r >= 0x2e80 && r <= 0xa4cf && r != 0x303f,
|
||||
r >= 0xac00 && r <= 0xd7a3,
|
||||
r >= 0xf900 && r <= 0xfaff,
|
||||
r >= 0xfe10 && r <= 0xfe19,
|
||||
r >= 0xfe30 && r <= 0xfe6f,
|
||||
r >= 0xff00 && r <= 0xff60,
|
||||
r >= 0xffe0 && r <= 0xffe6,
|
||||
r >= 0x20000 && r <= 0x2fffd,
|
||||
r >= 0x30000 && r <= 0x3fffd:
|
||||
return 2
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func minInt(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func baseName(path string) string {
|
||||
if path == "" {
|
||||
return ""
|
||||
}
|
||||
path = strings.ReplaceAll(path, "\\", "/")
|
||||
if idx := strings.LastIndex(path, "/"); idx >= 0 {
|
||||
return path[idx+1:]
|
||||
}
|
||||
return path
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package ui
|
||||
|
||||
import "os"
|
||||
|
||||
// IsTerminal 判断输出环境,是否是输出到终端界面,如果重定向到文件则不用进行下载统计打印输出了
|
||||
func IsTerminal(file *os.File) bool {
|
||||
if file == nil {
|
||||
return false
|
||||
}
|
||||
fi, err := file.Stat()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return (fi.Mode() & os.ModeCharDevice) != 0
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
//go:build !windows
|
||||
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package ui
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
func terminalSize() (int, int) {
|
||||
ws, err := unix.IoctlGetWinsize(int(os.Stdout.Fd()), unix.TIOCGWINSZ)
|
||||
if err != nil || ws == nil || ws.Col == 0 || ws.Row == 0 {
|
||||
return 80, 24
|
||||
}
|
||||
return int(ws.Col), int(ws.Row)
|
||||
}
|
||||
|
||||
func enableVirtualTerminalProcessing() {}
|
||||
@@ -0,0 +1,46 @@
|
||||
//go:build windows
|
||||
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package ui
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
func terminalSize() (int, int) {
|
||||
handle := windows.Handle(os.Stdout.Fd())
|
||||
var info windows.ConsoleScreenBufferInfo
|
||||
if err := windows.GetConsoleScreenBufferInfo(handle, &info); err != nil {
|
||||
return 80, 24
|
||||
}
|
||||
width := int(info.Window.Right - info.Window.Left + 1)
|
||||
height := int(info.Window.Bottom - info.Window.Top + 1)
|
||||
if width <= 0 || height <= 0 {
|
||||
return 80, 24
|
||||
}
|
||||
return width, height
|
||||
}
|
||||
|
||||
func enableVirtualTerminalProcessing() {
|
||||
handle := windows.Handle(os.Stdout.Fd())
|
||||
var mode uint32
|
||||
if err := windows.GetConsoleMode(handle, &mode); err != nil {
|
||||
return
|
||||
}
|
||||
mode |= windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING
|
||||
_ = windows.SetConsoleMode(handle, mode)
|
||||
}
|
||||
+147
-4
@@ -16,15 +16,13 @@ package utils
|
||||
import (
|
||||
"compress/gzip"
|
||||
"crypto/md5"
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/library-go/ids"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"net/http"
|
||||
"net/http/cookiejar"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -34,6 +32,11 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
jsoniter "github.com/json-iterator/go"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/library-go/ids"
|
||||
)
|
||||
|
||||
// TrimPathPrefix 去除目录的前缀
|
||||
@@ -301,3 +304,143 @@ func UnixTime2LocalFormatStr(unixTime int64) string {
|
||||
cz := time.FixedZone("CST", 8*3600) // 东8区
|
||||
return t.In(cz).Format("2006-01-02T15:04:05.000Z")
|
||||
}
|
||||
|
||||
// FormatSpeedFixedWidth 格式化速度显示
|
||||
// speed: 速度值,单位 Byte/s
|
||||
// 返回格式: " xxx.xx MB/s" (固定长度,左侧用空格补齐)
|
||||
func FormatSpeedFixedWidth(speed int64, totalWidth int) string {
|
||||
const (
|
||||
kb = 1024.0
|
||||
mb = kb * 1024
|
||||
gb = mb * 1024
|
||||
tb = gb * 1024
|
||||
)
|
||||
|
||||
var value float64
|
||||
var unit string
|
||||
|
||||
speedFloat := float64(speed)
|
||||
|
||||
switch {
|
||||
case speedFloat >= tb:
|
||||
value = speedFloat / tb
|
||||
unit = "TB/s"
|
||||
case speedFloat >= gb:
|
||||
value = speedFloat / gb
|
||||
unit = "GB/s"
|
||||
case speedFloat >= mb:
|
||||
value = speedFloat / mb
|
||||
unit = "MB/s"
|
||||
case speedFloat >= kb:
|
||||
value = speedFloat / kb
|
||||
unit = "KB/s"
|
||||
default:
|
||||
value = speedFloat
|
||||
unit = "B/s"
|
||||
}
|
||||
|
||||
// 格式化为两位小数
|
||||
formatted := fmt.Sprintf("%.2f %s", value, unit)
|
||||
|
||||
// 如果不足指定宽度,左侧用空格补齐
|
||||
if len(formatted) < totalWidth {
|
||||
spaces := strings.Repeat(" ", totalWidth-len(formatted))
|
||||
return spaces + formatted
|
||||
}
|
||||
|
||||
return formatted
|
||||
}
|
||||
|
||||
func GetPublicIP() (string, error) {
|
||||
type IPResponse struct {
|
||||
Origin string `json:"origin"`
|
||||
}
|
||||
resp, err := http.Get("https://httpbin.org/ip")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("请求失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return "", fmt.Errorf("HTTP 状态码异常: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("读取响应体失败: %w", err)
|
||||
}
|
||||
|
||||
var ipResp IPResponse
|
||||
if err := json.Unmarshal(body, &ipResp); err != nil {
|
||||
return "", fmt.Errorf("解析 JSON 失败: %w", err)
|
||||
}
|
||||
|
||||
if ipResp.Origin == "" {
|
||||
return "", fmt.Errorf("响应中无 IP 信息")
|
||||
}
|
||||
|
||||
// 取第一个 IP(处理代理/X-Forwarded-For 情况)
|
||||
ip := strings.TrimSpace(strings.Split(ipResp.Origin, ",")[0])
|
||||
return ip, nil
|
||||
}
|
||||
|
||||
// GetExpiredTimeSecFromOSSUrl 从OSS URL中解析出统一的过期时间戳(秒)
|
||||
func GetExpiredTimeSecFromOSSUrl(rawURL string) (int64, error) {
|
||||
parsedURL, err := url.Parse(rawURL)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to parse URL: %w", err)
|
||||
}
|
||||
|
||||
query := parsedURL.Query()
|
||||
sigVersion := query.Get("x-oss-signature-version")
|
||||
|
||||
var expires int64 = 0
|
||||
|
||||
// 检测版本标识(不区分大小写)
|
||||
if strings.Contains(sigVersion, "OSS4") {
|
||||
// V4 签名:x-oss-expires 为相对秒数,需要结合 x-oss-date
|
||||
expiresStr := query.Get("x-oss-expires")
|
||||
if expiresStr == "" {
|
||||
return 0, fmt.Errorf("V4 signature: 'x-oss-expires' parameter not found")
|
||||
}
|
||||
relSec, err := strconv.ParseInt(expiresStr, 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("V4 signature: invalid 'x-oss-expires' value: %w", err)
|
||||
}
|
||||
|
||||
dateStr := query.Get("x-oss-date")
|
||||
if dateStr == "" {
|
||||
return 0, fmt.Errorf("V4 signature: 'x-oss-date' parameter not found")
|
||||
}
|
||||
// 解析 ISO8601 UTC 时间,例如 "20231220T083018Z"
|
||||
t, err := time.Parse("20060102T150405Z", dateStr)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("V4 signature: failed to parse 'x-oss-date': %w", err)
|
||||
}
|
||||
expires = t.Unix() + relSec
|
||||
|
||||
} else if strings.Contains(sigVersion, "OSS3") || strings.Contains(sigVersion, "OSS2") {
|
||||
// V2 / V3:x-oss-expires 为绝对时间戳
|
||||
expiresStr := query.Get("x-oss-expires")
|
||||
if expiresStr == "" {
|
||||
return 0, fmt.Errorf("V2/V3 signature: 'x-oss-expires' parameter not found")
|
||||
}
|
||||
expires, err = strconv.ParseInt(expiresStr, 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("V2/V3 signature: invalid 'x-oss-expires' value: %w", err)
|
||||
}
|
||||
|
||||
} else {
|
||||
// 默认为 V1 签名(没有 x-oss-signature-version 或为空)
|
||||
expiresStr := query.Get("Expires")
|
||||
if expiresStr == "" {
|
||||
return 0, fmt.Errorf("V1 signature: 'Expires' parameter not found")
|
||||
}
|
||||
expires, err = strconv.ParseInt(expiresStr, 10, 64)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("V1 signature: invalid 'Expires' value: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return expires, nil
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan/internal/global"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -37,9 +36,12 @@ import (
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdutil"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdutil/escaper"
|
||||
"github.com/tickstep/aliyunpan/internal/command"
|
||||
"github.com/tickstep/aliyunpan/internal/command_local"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/global"
|
||||
"github.com/tickstep/aliyunpan/internal/panupdate"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/aliyunpan/library/homedir"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/urfave/cli"
|
||||
@@ -52,7 +54,7 @@ const (
|
||||
|
||||
var (
|
||||
// Version 版本号
|
||||
Version = "v0.3.6"
|
||||
Version = "v0.4.0"
|
||||
|
||||
// 命令历史文件
|
||||
historyFilePath = filepath.Join(config.GetConfigDir(), "aliyunpan_command_history.txt")
|
||||
@@ -83,16 +85,14 @@ func init() {
|
||||
|
||||
func checkLoginExpiredAndRelogin() {
|
||||
command.ReloadConfigFunc(nil)
|
||||
activeUser := config.Config.ActiveUser()
|
||||
if activeUser == nil || activeUser.UserId == "" {
|
||||
// maybe expired, try to login
|
||||
command.TryLogin()
|
||||
} else {
|
||||
// 尝试登录
|
||||
activeUser := command.TryLogin()
|
||||
if activeUser != nil && activeUser.UserId != "" {
|
||||
// 刷新过期Token并保存到配置文件
|
||||
command.RefreshWebTokenInNeed(activeUser, config.Config.DeviceName)
|
||||
command.RefreshWebTokenInNeed(activeUser)
|
||||
command.RefreshOpenTokenInNeed(activeUser)
|
||||
command.SaveConfigFunc(nil)
|
||||
}
|
||||
command.SaveConfigFunc(nil)
|
||||
}
|
||||
|
||||
func main() {
|
||||
@@ -102,20 +102,7 @@ func main() {
|
||||
checkLoginExpiredAndRelogin()
|
||||
|
||||
// check token expired task
|
||||
go func() {
|
||||
for {
|
||||
// Token刷新进程,不管是CLI命令行模式,还是直接命令模式,本刷新任务都会执行
|
||||
time.Sleep(time.Duration(1) * time.Minute)
|
||||
//time.Sleep(time.Duration(5) * time.Second)
|
||||
for _, u := range config.Config.UserList {
|
||||
if u.UserId == config.Config.ActiveUID {
|
||||
if u.PanClient() != nil {
|
||||
checkLoginExpiredAndRelogin()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
command.AutomaticallyRefreshTokenTask() // Token刷新进程,不管是CLI命令行模式,还是直接命令模式,本刷新任务都会执行
|
||||
|
||||
app := cli.NewApp()
|
||||
cmder.SetApp(app)
|
||||
@@ -123,7 +110,7 @@ func main() {
|
||||
app.Name = "aliyunpan"
|
||||
app.Version = Version
|
||||
app.Author = "tickstep/aliyunpan: https://github.com/tickstep/aliyunpan"
|
||||
app.Copyright = "(c) 2021-2025 tickstep."
|
||||
app.Copyright = "(c) 2021-2026 tickstep."
|
||||
app.Usage = "阿里云盘客户端 for " + runtime.GOOS + "/" + runtime.GOARCH
|
||||
app.Description = `aliyunpan 是一款阿里云盘命令行客户端工具, 为操作阿里云盘, 提供实用功能。
|
||||
支持同步备份功能,支持备份本地文件到云盘,备份云盘文件到本地。
|
||||
@@ -185,23 +172,33 @@ func main() {
|
||||
|
||||
// tab 自动补全命令
|
||||
line.State.SetCompleter(func(line string) (s []string) {
|
||||
var (
|
||||
lineArgs = args.Parse(line)
|
||||
numArgs = len(lineArgs)
|
||||
acceptCompleteFileCommands = []string{
|
||||
"cd", "cp", "xcp", "download", "ls", "mkdir", "mv", "pwd", "rename", "rm", "share", "save", "upload", "login", "loglist", "logout",
|
||||
"clear", "quit", "exit", "quota", "who", "sign", "update", "who", "su", "config",
|
||||
"drive", "export", "import", "sync", "tree",
|
||||
}
|
||||
closed = strings.LastIndex(line, " ") == len(line)-1
|
||||
)
|
||||
// 命令补全处理器
|
||||
// line: 输入的命令行完整字符,包括命令名称、空格、(完整或部分)路径字符
|
||||
// s: 返回的满足符合条件的提示列表,命令名称、或者文件完整路径
|
||||
|
||||
var (
|
||||
lineArgs = args.Parse(line)
|
||||
numArgs = len(lineArgs)
|
||||
// 支持TAB补全文件路径的命令
|
||||
acceptCompleteFilePanCommands = []string{ // 云盘命令
|
||||
"cd", "cp", "xcp", "download", "ls", "mkdir", "mv", "rename", "rm", "upload", "tree",
|
||||
}
|
||||
acceptCompleteFileLocalCommands = []string{ // 本地命令
|
||||
"lcd", "lls",
|
||||
}
|
||||
closed = false
|
||||
)
|
||||
// 检查是否是命令输入的结尾
|
||||
if strings.LastIndex(line, " ") == len(line)-1 {
|
||||
closed = true
|
||||
}
|
||||
|
||||
// 开始补全命令名称
|
||||
for _, cmd := range app.Commands {
|
||||
for _, name := range cmd.Names() {
|
||||
if !strings.HasPrefix(name, line) {
|
||||
continue
|
||||
}
|
||||
|
||||
s = append(s, name+" ")
|
||||
}
|
||||
}
|
||||
@@ -215,108 +212,25 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// 开始补全文件路径
|
||||
thisCmd := app.Command(lineArgs[0])
|
||||
if thisCmd == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !cmdutil.ContainsString(acceptCompleteFileCommands, thisCmd.FullName()) {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
activeUser = config.Config.ActiveUser()
|
||||
runeFunc = unicode.IsSpace
|
||||
cmdRuneFunc = func(r rune) bool {
|
||||
switch r {
|
||||
case '\'', '"':
|
||||
return true
|
||||
}
|
||||
return unicode.IsSpace(r)
|
||||
// 检测输入的命令是否支持tab补全路径
|
||||
if cmdutil.ContainsString(acceptCompleteFilePanCommands, thisCmd.FullName()) {
|
||||
// 云盘文件路径的补全
|
||||
if thisCmd.FullName() == "ls" || thisCmd.FullName() == "cd" {
|
||||
// 只需要补全文件夹路径
|
||||
return panFilePathCompleter(line, lineArgs, numArgs, true)
|
||||
} else {
|
||||
// 文件夹、文件都需要
|
||||
return panFilePathCompleter(line, lineArgs, numArgs, false)
|
||||
}
|
||||
targetPath string
|
||||
)
|
||||
|
||||
if activeUser == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if !closed {
|
||||
targetPath = lineArgs[numArgs-1]
|
||||
escaper.EscapeStringsByRuneFunc(lineArgs[:numArgs-1], runeFunc) // 转义
|
||||
} else {
|
||||
escaper.EscapeStringsByRuneFunc(lineArgs, runeFunc)
|
||||
}
|
||||
|
||||
switch {
|
||||
case targetPath == "." || strings.HasSuffix(targetPath, "/."):
|
||||
s = append(s, line+"/")
|
||||
return
|
||||
case targetPath == ".." || strings.HasSuffix(targetPath, "/.."):
|
||||
s = append(s, line+"/")
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
targetDir string
|
||||
isAbs = path.IsAbs(targetPath)
|
||||
isDir = strings.LastIndex(targetPath, "/") == len(targetPath)-1
|
||||
)
|
||||
|
||||
if isAbs {
|
||||
targetDir = path.Dir(targetPath)
|
||||
} else {
|
||||
wd := "/"
|
||||
if activeUser.IsFileDriveActive() {
|
||||
wd = activeUser.Workdir
|
||||
} else if activeUser.IsResourceDriveActive() {
|
||||
wd = activeUser.ResourceWorkdir
|
||||
} else if activeUser.IsAlbumDriveActive() {
|
||||
wd = activeUser.AlbumWorkdir
|
||||
}
|
||||
targetDir = path.Join(wd, targetPath)
|
||||
if !isDir {
|
||||
targetDir = path.Dir(targetDir)
|
||||
}
|
||||
}
|
||||
|
||||
// tab键补全路径
|
||||
files, err := activeUser.CacheFilesDirectoriesList(targetDir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, file := range files {
|
||||
if file == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
var (
|
||||
appendLine string
|
||||
)
|
||||
|
||||
// 已经有的情况
|
||||
if !closed {
|
||||
if !strings.HasPrefix(file.Path, path.Clean(path.Join(targetDir, path.Base(targetPath)))) {
|
||||
if path.Base(targetDir) == path.Base(targetPath) {
|
||||
appendLine = strings.Join(append(lineArgs[:numArgs-1], escaper.EscapeByRuneFunc(path.Join(targetPath, file.FileName), cmdRuneFunc)), " ")
|
||||
goto handle
|
||||
}
|
||||
continue
|
||||
}
|
||||
appendLine = strings.Join(append(lineArgs[:numArgs-1], escaper.EscapeByRuneFunc(path.Clean(path.Join(path.Dir(targetPath), file.FileName)), cmdRuneFunc)), " ")
|
||||
goto handle
|
||||
}
|
||||
// 没有的情况
|
||||
appendLine = strings.Join(append(lineArgs, escaper.EscapeByRuneFunc(file.FileName, cmdRuneFunc)), " ")
|
||||
goto handle
|
||||
|
||||
handle:
|
||||
if file.IsFolder() {
|
||||
s = append(s, appendLine+"/")
|
||||
continue
|
||||
}
|
||||
s = append(s, appendLine+" ")
|
||||
continue
|
||||
} else if cmdutil.ContainsString(acceptCompleteFileLocalCommands, thisCmd.FullName()) {
|
||||
// 本地文件路径的补全
|
||||
return localFilePathCompleter(line, lineArgs, numArgs, true)
|
||||
}
|
||||
return
|
||||
})
|
||||
@@ -325,8 +239,10 @@ func main() {
|
||||
fmt.Printf("提示: Ctrl + A / E 跳转命令 首 / 尾.\n")
|
||||
fmt.Printf("提示: 输入 help 获取帮助.\n")
|
||||
|
||||
// check update
|
||||
// 刷新配置文件
|
||||
command.ReloadConfigFunc(c)
|
||||
|
||||
// check update
|
||||
if config.Config.UpdateCheckInfo.LatestVer != "" {
|
||||
if utils.ParseVersionNum(config.Config.UpdateCheckInfo.LatestVer) > utils.ParseVersionNum(global.AppVersion) {
|
||||
fmt.Printf("\n当前的软件版本为:%s, 现在有新版本 %s 可供更新,强烈推荐进行更新!(可以输入 update 命令进行更新)\n\n",
|
||||
@@ -334,6 +250,7 @@ func main() {
|
||||
}
|
||||
}
|
||||
go func() {
|
||||
// 新版本检测异步任务
|
||||
latestCheckTime := config.Config.UpdateCheckInfo.CheckTime
|
||||
nowTime := time.Now().Unix()
|
||||
secsOf12Hour := int64(43200)
|
||||
@@ -351,14 +268,22 @@ func main() {
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
for { // 命令行交互进程,每一次命令执行都会进入到这个for循环
|
||||
var (
|
||||
prompt string
|
||||
activeUser = config.Config.ActiveUser()
|
||||
)
|
||||
|
||||
if activeUser == nil {
|
||||
// 尝试重新登录并获取新的token
|
||||
activeUser = command.TryLogin()
|
||||
|
||||
// 自动登录成功
|
||||
if activeUser != nil {
|
||||
logger.Verboseln("自动重新登录成功")
|
||||
// 保存新token到配置文件
|
||||
command.SaveConfigFunc(c)
|
||||
}
|
||||
}
|
||||
|
||||
if activeUser != nil && activeUser.Nickname != "" {
|
||||
@@ -381,14 +306,14 @@ func main() {
|
||||
prompt = app.Name + " > "
|
||||
}
|
||||
|
||||
commandLine, err := line.State.Prompt(prompt)
|
||||
switch err {
|
||||
commandLine, err1 := line.State.Prompt(prompt)
|
||||
switch err1 {
|
||||
case liner.ErrPromptAborted:
|
||||
return
|
||||
case nil:
|
||||
// continue
|
||||
default:
|
||||
fmt.Println(err)
|
||||
fmt.Println(err1)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -680,6 +605,13 @@ func main() {
|
||||
},
|
||||
},
|
||||
|
||||
// 当前本地工作目录
|
||||
command_local.CmdLocalPwd(),
|
||||
// 切换本地工作目录
|
||||
command_local.CmdLocalCd(),
|
||||
// 展示本地目录文件列表
|
||||
command_local.CmdLocalLs(),
|
||||
|
||||
// 调试用 debug
|
||||
//{
|
||||
// Name: "debug",
|
||||
@@ -730,3 +662,247 @@ func main() {
|
||||
sort.Sort(cli.CommandsByName(app.Commands))
|
||||
app.Run(os.Args)
|
||||
}
|
||||
|
||||
// panFilePathCompleter 云盘文件路径Tab补全处理器
|
||||
func panFilePathCompleter(line string, lineArgs []string, numArgs int, onlyNeedFolder bool) (s []string) {
|
||||
return filePathCompleterProcessor("pan", line, lineArgs, numArgs, onlyNeedFolder, getPanFileListByTargetPath)
|
||||
}
|
||||
|
||||
// localFilePathCompleter 本地文件路径Tab补全处理器
|
||||
func localFilePathCompleter(line string, lineArgs []string, numArgs int, onlyNeedFolder bool) (s []string) {
|
||||
return filePathCompleterProcessor("local", line, lineArgs, numArgs, onlyNeedFolder, getLocalFileListByTargetPath)
|
||||
}
|
||||
|
||||
// filePathCompleterProcessor 路径自动补充处理器
|
||||
// 典型测试样例
|
||||
// lls d:\
|
||||
// lls ~/
|
||||
// lls ~\
|
||||
// lls ~\Doc
|
||||
// ls ~/
|
||||
// ls<空格><tab>
|
||||
// ls app<tab>
|
||||
// ls /doc<tab>
|
||||
func filePathCompleterProcessor(targetType string, line string, lineArgs []string, numArgs int, onlyNeedFolder bool, fileListFunc getFileListByTargetPath) (s []string) {
|
||||
var (
|
||||
runeFunc = unicode.IsSpace
|
||||
cmdRuneFunc = func(r rune) bool {
|
||||
switch r {
|
||||
case '\'', '"':
|
||||
return true
|
||||
}
|
||||
return unicode.IsSpace(r)
|
||||
}
|
||||
targetPath string
|
||||
// 路径的结尾是否是有前缀,如果有前缀则文件、文件夹名称需要匹配前缀
|
||||
endPathHasPrefixStr = false
|
||||
)
|
||||
if numArgs > 1 {
|
||||
targetPath = lineArgs[numArgs-1]
|
||||
} else {
|
||||
// 没有输入文件路径的时候,默认为当前目录
|
||||
targetPath = "./"
|
||||
line += targetPath
|
||||
lineArgs = append(lineArgs, targetPath)
|
||||
numArgs = 2
|
||||
}
|
||||
switch {
|
||||
case targetPath == "." || strings.HasSuffix(targetPath, "/."):
|
||||
s = append(s, line+"/")
|
||||
return
|
||||
case targetPath == ".." || strings.HasSuffix(targetPath, "/.."):
|
||||
s = append(s, line+"/")
|
||||
return
|
||||
}
|
||||
// 主目录"~"特殊路径处理
|
||||
if strings.HasPrefix(targetPath, "~") {
|
||||
if targetType == "local" {
|
||||
if p, e := homedir.Expand(targetPath); e == nil {
|
||||
targetPath = p
|
||||
}
|
||||
} else {
|
||||
// 云盘的主目录~默认为根目录
|
||||
targetPath = strings.ReplaceAll(strings.Replace(targetPath, "~", "/", 1), "//", "/")
|
||||
}
|
||||
}
|
||||
// 完整路径是需要“/”作为结尾,否则默认用户只输入了一部分文件名,输入的部分字符作为文件名前缀进行搜索匹配
|
||||
// 如果用户输入:/Users/tickstep/doc,则自动补全的路径需要在/Users/tickstep/目录下搜索并且文件名必须包含doc这个前缀,例如:/Users/tickstep/document/ 和 /Users/tickstep/doc/ 都是匹配项
|
||||
// 如果用户输入:/Users/tickstep/,则自动补全的路径需要在/Users/tickstep/目录下搜索即可,例如:/Users/tickstep/photo/ 和 /Users/tickstep/doc/ 都是匹配项
|
||||
if strings.HasSuffix(targetPath, "/") {
|
||||
endPathHasPrefixStr = false
|
||||
} else {
|
||||
endPathHasPrefixStr = true
|
||||
}
|
||||
|
||||
if endPathHasPrefixStr {
|
||||
escaper.EscapeStringsByRuneFunc(lineArgs[:numArgs-1], runeFunc) // 转义
|
||||
} else {
|
||||
escaper.EscapeStringsByRuneFunc(lineArgs, runeFunc)
|
||||
}
|
||||
|
||||
targetFullPath, targetDir, files := fileListFunc(targetPath)
|
||||
for _, file := range files {
|
||||
if file == nil {
|
||||
continue
|
||||
}
|
||||
if onlyNeedFolder && !file.IsFolder {
|
||||
// 过滤文件,只需要文件夹
|
||||
continue
|
||||
}
|
||||
|
||||
var (
|
||||
appendLine string
|
||||
)
|
||||
|
||||
// 用户已经输入了部分路径字符,搜索匹配文件路径需要匹配用户输入的路径字符串前缀
|
||||
if endPathHasPrefixStr {
|
||||
fp := command_local.LocalPathClean(file.Path)
|
||||
tp := command_local.LocalPathClean(targetFullPath)
|
||||
if !strings.HasPrefix(fp, tp) {
|
||||
if command_local.LocalPathBase(targetDir) == command_local.LocalPathBase(targetFullPath) {
|
||||
appendLine = strings.Join(append(lineArgs[:numArgs-1], escaper.EscapeByRuneFunc(path.Join(targetPath, file.FileName), cmdRuneFunc)), " ")
|
||||
goto handle
|
||||
}
|
||||
continue
|
||||
}
|
||||
appendLine = strings.Join(append(lineArgs[:numArgs-1], escaper.EscapeByRuneFunc(command_local.LocalPathClean(path.Join(command_local.LocalPathDir(targetPath), file.FileName)), cmdRuneFunc)), " ")
|
||||
goto handle
|
||||
}
|
||||
// 没有输入任何路径前缀字符,默认都符合条件,全部显示到提示列表
|
||||
appendLine = strings.Join(append(lineArgs[:numArgs-1], escaper.EscapeByRuneFunc(command_local.LocalPathClean(path.Join(targetPath, file.FileName)), cmdRuneFunc)), " ")
|
||||
goto handle
|
||||
|
||||
handle:
|
||||
if file.IsFolder {
|
||||
s = append(s, appendLine+"/")
|
||||
continue
|
||||
}
|
||||
s = append(s, appendLine+" ")
|
||||
continue
|
||||
}
|
||||
|
||||
// 没有命中的路径,返回当前用户输入的内容即可
|
||||
if len(s) == 0 {
|
||||
s = append(s, line)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type fileEntity struct {
|
||||
FileName string
|
||||
Path string
|
||||
IsFolder bool
|
||||
}
|
||||
type getFileListByTargetPath func(targetPath string) (string, string, []*fileEntity)
|
||||
|
||||
// getPanFileListByTargetPath 通过路径获取云盘文件列表
|
||||
func getPanFileListByTargetPath(targetPath string) (string, string, []*fileEntity) {
|
||||
var (
|
||||
activeUser = config.Config.ActiveUser()
|
||||
targetDir string
|
||||
targetFullPath string
|
||||
isAbs = path.IsAbs(targetPath)
|
||||
isDir = strings.LastIndex(targetPath, "/") == len(targetPath)-1
|
||||
r = []*fileEntity{}
|
||||
)
|
||||
if activeUser == nil {
|
||||
return targetFullPath, targetDir, r
|
||||
}
|
||||
|
||||
if isAbs {
|
||||
targetDir = path.Dir(targetPath)
|
||||
targetFullPath = targetPath
|
||||
} else {
|
||||
wd := "/"
|
||||
if activeUser.IsFileDriveActive() {
|
||||
wd = activeUser.Workdir
|
||||
} else if activeUser.IsResourceDriveActive() {
|
||||
wd = activeUser.ResourceWorkdir
|
||||
} else if activeUser.IsAlbumDriveActive() {
|
||||
wd = activeUser.AlbumWorkdir
|
||||
}
|
||||
targetFullPath = path.Join(wd, targetPath)
|
||||
targetDir = targetFullPath
|
||||
if !isDir {
|
||||
targetDir = path.Dir(targetDir)
|
||||
}
|
||||
}
|
||||
|
||||
files, err := activeUser.CacheFilesDirectoriesList(targetDir)
|
||||
if err != nil {
|
||||
return targetFullPath, targetDir, r
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
if file == nil {
|
||||
continue
|
||||
}
|
||||
r = append(r, &fileEntity{
|
||||
FileName: file.FileName,
|
||||
Path: file.Path,
|
||||
IsFolder: file.IsFolder(),
|
||||
})
|
||||
}
|
||||
return targetFullPath, targetDir, r
|
||||
}
|
||||
|
||||
// getLocalFileListByTargetPath 通过路径获取本地文件列表
|
||||
func getLocalFileListByTargetPath(targetPath string) (string, string, []*fileEntity) {
|
||||
targetPath = strings.ReplaceAll(targetPath, "\\", "/")
|
||||
var (
|
||||
targetDir string
|
||||
targetFullPath string
|
||||
isAbs = filepath.IsAbs(targetPath)
|
||||
isDir = strings.HasSuffix(targetPath, "/")
|
||||
r = []*fileEntity{}
|
||||
)
|
||||
|
||||
if isAbs {
|
||||
// 绝对路径
|
||||
targetDir = targetPath
|
||||
targetFullPath = targetPath
|
||||
if !isDir {
|
||||
targetDir = command_local.LocalPathDir(targetDir)
|
||||
}
|
||||
} else {
|
||||
// 相对路径
|
||||
targetFullPath = command_local.LocalPathJoin(targetPath)
|
||||
targetDir = targetFullPath
|
||||
if !isDir {
|
||||
targetDir = command_local.LocalPathDir(targetDir)
|
||||
}
|
||||
}
|
||||
// windows
|
||||
if runtime.GOOS == "windows" {
|
||||
if strings.HasSuffix(targetDir, ":") {
|
||||
targetDir += "/"
|
||||
}
|
||||
}
|
||||
|
||||
fileEntryList, err := os.ReadDir(targetDir)
|
||||
if err != nil {
|
||||
return targetFullPath, targetDir, r
|
||||
}
|
||||
for _, file := range fileEntryList {
|
||||
if file == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" { // windows隐藏文件
|
||||
if strings.HasPrefix(file.Name(), "$") {
|
||||
continue
|
||||
}
|
||||
} else { // linux / macOS隐藏文件
|
||||
if strings.HasPrefix(file.Name(), ".") {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
r = append(r, &fileEntity{
|
||||
FileName: file.Name(),
|
||||
Path: path.Join(targetDir, file.Name()),
|
||||
IsFolder: file.IsDir(),
|
||||
})
|
||||
}
|
||||
return targetFullPath, targetDir, r
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
+7
-7
@@ -2,14 +2,14 @@
|
||||
"FixedFileInfo": {
|
||||
"FileVersion": {
|
||||
"Major": 0,
|
||||
"Minor": 3,
|
||||
"Patch": 6,
|
||||
"Minor": 4,
|
||||
"Patch": 0,
|
||||
"Build": 0
|
||||
},
|
||||
"ProductVersion": {
|
||||
"Major": 0,
|
||||
"Minor": 3,
|
||||
"Patch": 6,
|
||||
"Minor": 4,
|
||||
"Patch": 0,
|
||||
"Build": 0
|
||||
},
|
||||
"FileFlagsMask": "3f",
|
||||
@@ -22,14 +22,14 @@
|
||||
"Comments": "",
|
||||
"CompanyName": "tickstep",
|
||||
"FileDescription": "阿里云盘客户端",
|
||||
"FileVersion": "v0.3.6",
|
||||
"FileVersion": "v0.4.0",
|
||||
"InternalName": "",
|
||||
"LegalCopyright": "© 2021-2025 tickstep.",
|
||||
"LegalCopyright": "© 2021-2026 tickstep.",
|
||||
"LegalTrademarks": "",
|
||||
"OriginalFilename": "",
|
||||
"PrivateBuild": "",
|
||||
"ProductName": "aliyunpan",
|
||||
"ProductVersion": "v0.3.6",
|
||||
"ProductVersion": "v0.4.0",
|
||||
"SpecialBuild": ""
|
||||
},
|
||||
"VarFileInfo": {
|
||||
|
||||
Reference in New Issue
Block a user