mirror of
https://gitee.com/zhangsken/aliyunpan.git
synced 2026-10-16 10:29:42 +08:00
Compare commits
120 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 | |||
| 057362d5a9 | |||
| ae3590f84a | |||
| 770cd0356f | |||
| 12bca5683c | |||
| 68f4c44a55 | |||
| 3aae75a46e | |||
| 44e7402b27 | |||
| 499b08dd72 | |||
| 29ba6903e6 | |||
| 6d1687e519 | |||
| d8d00600d8 | |||
| 304c4d4f93 | |||
| 4c75a3d6ee | |||
| 34d58d3ebc | |||
| 8410f5977c | |||
| 9730bb8a23 | |||
| 6ed0e6bae0 | |||
| 2b283b727e | |||
| f3d352800b | |||
| 2eaba0c3c8 | |||
| 4a59f246f2 | |||
| d45ef8c1b9 | |||
| ed26496bb7 | |||
| 1e2bcee3e7 | |||
| 1415e81eec | |||
| 7ebaf6ec17 | |||
| 1e205b52a3 | |||
| 790056a488 | |||
| 68c89ec317 | |||
| 2849dbb472 | |||
| 68437a7a4c | |||
| 05f695a882 | |||
| fbfa313c1e | |||
| ff97c172b6 | |||
| 47baab44a7 | |||
| 0b5bd60702 | |||
| 6f45a8048d | |||
| 7132342ed3 |
@@ -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功能。" -->
|
||||
|
||||
### Ⅲ. 请描述新功能的使用场景
|
||||
<!-- 可以讲讲你对新功能的看法,为什么需要增加这个功能,这个新功能在哪些场景下会使用。 -->
|
||||
@@ -4,17 +4,18 @@
|
||||
# 特色
|
||||
1. 多平台支持, 支持 Windows, macOS, linux(x86/x64/arm), android, iOS 等
|
||||
2. 阿里云盘多用户支持
|
||||
3. 支持同步盘,资源库无缝切换
|
||||
3. 支持备份盘,资源库无缝切换
|
||||
4. [下载](docs/manual.md#下载文件目录)网盘内文件, 支持多个文件或目录下载, 支持断点续传和单文件并行下载。支持软链接(符号链接)文件。
|
||||
5. [上传](docs/manual.md#上传文件目录)本地文件, 支持多个文件或目录上传,支持排除指定文件夹/文件(正则表达式)功能。支持软链接(符号链接)文件。
|
||||
6. [同步备份功能](docs/manual.md#同步备份功能)支持备份本地文件到云盘,备份云盘文件到本地,双向同步备份保持本地文件和网盘文件同步。常用于嵌入式或者NAS等设备,支持docker镜像部署。
|
||||
7. 命令和文件路径输入支持Tab键自动补全,路径支持通配符匹配模式
|
||||
8. 支持[JavaScript插件](docs/manual.md#JavaScript插件),你可以按照自己的需要定制上传/下载中关键步骤的行为,最大程度满足自己的个性化需求
|
||||
9. 支持个人相册的相关操作,支持批量下载相册所有照片、视频文件到本地
|
||||
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.3/aliyunpan-v0.3.3-linux-amd64.zip
|
||||
unzip aliyunpan-v0.3.3-linux-amd64.zip
|
||||
cd aliyunpan-v0.3.3-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.3
|
||||
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.3.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"
|
||||
|
||||
+10
-2
@@ -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"
|
||||
@@ -13,6 +16,9 @@ LABEL description="sync & backup service for aliyun cloud drive"
|
||||
ENV TZ=Asia/Shanghai
|
||||
# 手动下载tzdata安装包,注意要下载对应架构的: https://dl-cdn.alpinelinux.org/alpine/v3.15/community/
|
||||
RUN apk add -U tzdata
|
||||
# 更新Alpine的软件源为国内镜像站点,提高下载速度。安装tzdata包
|
||||
# RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
||||
# RUN apk add --no-cache tzdata
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
||||
|
||||
# 创建运行目录
|
||||
@@ -61,7 +67,9 @@ ENV ALIYUNPAN_SYNC_CYCLE=infinity
|
||||
ENV ALIYUNPAN_SYNC_PRIORITY=time
|
||||
ENV ALIYUNPAN_SYNC_LOG=false
|
||||
ENV ALIYUNPAN_LOCAL_DELAY_TIME=3
|
||||
ENV ALIYUNPAN_SCAN_INTERVAL_TIME=1
|
||||
ENV ALIYUNPAN_DEVICE_ID=""
|
||||
ENV ALIYUNPAN_IP_TYPE="ipv4"
|
||||
|
||||
# 运行
|
||||
ENTRYPOINT ./app.sh
|
||||
+8
-1
@@ -39,6 +39,9 @@ sleep 2s
|
||||
# ./aliyunpan config set -device_id ${ALIYUNPAN_DEVICE_ID}
|
||||
#fi
|
||||
|
||||
# show docker IPs
|
||||
./aliyunpan tool getip
|
||||
|
||||
# check login already or not
|
||||
./aliyunpan who
|
||||
if [ $? -eq 0 ]
|
||||
@@ -56,4 +59,8 @@ else
|
||||
./aliyunpan config set -file_record_config 2
|
||||
fi
|
||||
|
||||
./aliyunpan sync start -dp ${ALIYUNPAN_DOWNLOAD_PARALLEL} -up ${ALIYUNPAN_UPLOAD_PARALLEL} -dbs ${ALIYUNPAN_DOWNLOAD_BLOCK_SIZE} -ubs ${ALIYUNPAN_UPLOAD_BLOCK_SIZE} -log ${ALIYUNPAN_SYNC_LOG} -ldt ${ALIYUNPAN_LOCAL_DELAY_TIME} -cycle ${ALIYUNPAN_SYNC_CYCLE}
|
||||
# IP type
|
||||
./aliyunpan config set -ip_type ${ALIYUNPAN_IP_TYPE}
|
||||
|
||||
# run
|
||||
./aliyunpan sync start -dp ${ALIYUNPAN_DOWNLOAD_PARALLEL} -up ${ALIYUNPAN_UPLOAD_PARALLEL} -dbs ${ALIYUNPAN_DOWNLOAD_BLOCK_SIZE} -ubs ${ALIYUNPAN_UPLOAD_BLOCK_SIZE} -log ${ALIYUNPAN_SYNC_LOG} -ldt ${ALIYUNPAN_LOCAL_DELAY_TIME} -cycle ${ALIYUNPAN_SYNC_CYCLE} -sit ${ALIYUNPAN_SCAN_INTERVAL_TIME}
|
||||
|
||||
@@ -38,4 +38,6 @@ services:
|
||||
# 是否显示文件备份过程日志,true-显示,false-不显示
|
||||
- ALIYUNPAN_SYNC_LOG=true
|
||||
# 本地文件修改检测延迟间隔,单位秒。如果本地文件会被频繁修改,例如录制视频文件,配置好该时间可以避免上传未录制好的文件
|
||||
- ALIYUNPAN_LOCAL_DELAY_TIME=3
|
||||
- ALIYUNPAN_LOCAL_DELAY_TIME=3
|
||||
# 扫描文件间隔时间,单位:分钟
|
||||
- ALIYUNPAN_SCAN_INTERVAL_TIME=1
|
||||
+147
-551
@@ -20,13 +20,16 @@
|
||||
* [创建目录](#创建目录)
|
||||
* [删除文件/目录](#删除文件目录)
|
||||
* [移动文件/目录](#移动文件目录)
|
||||
* [备份盘和资源库之间转存文件](#备份盘和资源库之间转存文件)
|
||||
* [重命名文件/目录](#重命名文件目录)
|
||||
* [分享文件/目录](#分享文件目录)
|
||||
+ [设置分享文件/目录](#设置分享文件目录)
|
||||
+ [创建快传链接](#创建快传链接)
|
||||
+ [列出已分享文件/目录](#列出已分享文件目录)
|
||||
+ [取消分享文件/目录](#取消分享文件目录)
|
||||
* [共享相册](#共享相册)
|
||||
+ [展示共享相簿列表](#展示共享相簿列表)
|
||||
+ [展示指定相簿中的文件](#展示指定相簿中的文件)
|
||||
+ [下载相簿中的所有文件](#下载相簿中的所有文件)
|
||||
* [同步备份功能](#同步备份功能)
|
||||
+ [常用命令说明](#常用命令说明)
|
||||
+ [备份配置文件说明](#备份配置文件说明)
|
||||
@@ -35,16 +38,8 @@
|
||||
+ [Windows后台启动](#Windows后台启动)
|
||||
+ [Docker运行](#Docker运行)
|
||||
* [JavaScript插件](#JavaScript插件)
|
||||
+ [如何使用](#如何使用)
|
||||
+ [JS中内置的函数](#JS中内置的函数)
|
||||
+ [常见场景样例](#常见场景样例)
|
||||
+ [1.禁止特定文件上传](#1.禁止特定文件上传)
|
||||
+ [2.上传文件后删除本地文件](#2.上传文件后删除本地文件)
|
||||
+ [3.下载文件并截断过长的文件名](#3.下载文件并截断过长的文件名)
|
||||
+ [4.上传文件去掉文件名包含的部分字符](#4.上传文件去掉文件名包含的部分字符)
|
||||
+ [5.Token刷新失败发送外部通知](#5.Token刷新失败发送外部通知)
|
||||
* [显示和修改程序配置项](#显示和修改程序配置项)
|
||||
- [常见问题Q&A](#常见问题Q&A)
|
||||
- [常见问题Q&A](#常见问题QA)
|
||||
* [1. 如何开启Debug调试日志](#1-如何开启Debug调试日志)
|
||||
|
||||
# 命令列表及说明
|
||||
@@ -221,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面板显示下载详情和进度,更加直观和友好
|
||||
```
|
||||
|
||||
|
||||
@@ -247,6 +244,9 @@ aliyunpan d /我的文档/1.mp4
|
||||
|
||||
# 下载 /我的文档 整个目录!!
|
||||
aliyunpan d /我的文档
|
||||
|
||||
# 下载 /photo 整个目录,并使用UI面板展示下载详情
|
||||
aliyunpan d /photo -ui
|
||||
```
|
||||
|
||||
下载的文件默认保存到 **程序所在目录** 的 download/ 目录, 支持设置指定目录, 重名的文件会自动跳过!
|
||||
@@ -254,6 +254,39 @@ aliyunpan d /我的文档
|
||||
通过 `aliyunpan config set -savedir <savedir>` 可以自定义保存的目录。
|
||||
支持多个文件或目录下载,支持自动跳过下载重名的文件!
|
||||
|
||||
### Linux后台下载
|
||||
需要结合nohup进行启动。
|
||||
|
||||
新建 download.sh 脚本,内容如下
|
||||
```
|
||||
#!/bin/bash
|
||||
|
||||
# 是否开启调试日志
|
||||
export ALIYUNPAN_VERBOSE=0
|
||||
# (可选)配置目录的绝对路径(请更改成你自己的目录)
|
||||
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" download --saveto "$LOCAL_DIR" "$PAN_DIR"
|
||||
```
|
||||
|
||||
增加脚本执行权限
|
||||
```
|
||||
$ chmod +x download.sh
|
||||
```
|
||||
|
||||
然后启动该脚本进行后台运行
|
||||
```
|
||||
$ nohup ./download.sh > aliyunpan.log 2>&1 &
|
||||
```
|
||||
|
||||
## 多用户联合下载
|
||||
前提:程序必须登录多个帐号,并且登录授权都有效。
|
||||
```
|
||||
@@ -285,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文件
|
||||
@@ -300,6 +336,54 @@ aliyunpan upload -exn "\.jpg$" -exn "\.mp3$" C:/Users/Administrator/Video /视
|
||||
4)排除~号开头的文件:-exn "^~"
|
||||
5)排除 myfile.txt 文件:-exn "^myfile.txt$"
|
||||
```
|
||||
### Linux后台上传
|
||||
需要结合nohup进行启动。
|
||||
|
||||
新建 upload.sh 脚本,内容如下
|
||||
```
|
||||
#!/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"
|
||||
|
||||
# 本地目录
|
||||
LOCAL_DIR="/tickstep/Documents/我的文档"
|
||||
|
||||
# 网盘目录
|
||||
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] 上传任务执行完毕"
|
||||
```
|
||||
|
||||
增加脚本执行权限
|
||||
```
|
||||
$ chmod +x upload.sh
|
||||
```
|
||||
|
||||
然后启动该脚本进行后台运行
|
||||
```
|
||||
$ nohup ./upload.sh > aliyunpan.log 2>&1 &
|
||||
```
|
||||
|
||||
## 创建目录
|
||||
```
|
||||
@@ -346,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 <旧文件/目录名> <新文件/目录名>
|
||||
@@ -406,6 +471,30 @@ aliyunpan share cancel <shareid_1> <shareid_2> ...
|
||||
```
|
||||
目前只支持通过分享id (shareid) 来取消分享.
|
||||
|
||||
## 共享相册
|
||||
```
|
||||
aliyunpan album
|
||||
```
|
||||
|
||||
### 展示共享相簿列表
|
||||
```
|
||||
aliyunpan album list
|
||||
```
|
||||
|
||||
### 展示指定相簿中的文件
|
||||
展示相簿中文件,同名的相簿只会展示第一个符合条件的
|
||||
```
|
||||
例如:展示相簿中文件"我的相簿2025"
|
||||
aliyunpan album list-file "我的相簿2025"
|
||||
```
|
||||
|
||||
### 下载相簿中的所有文件
|
||||
下载相簿中的所有文件到本地,支持普通照片、实况照片。
|
||||
```
|
||||
下载相簿 "我的相簿2025" 里面的所有文件
|
||||
aliyunpan album download-file 我的相簿2025
|
||||
```
|
||||
|
||||
## 同步备份功能
|
||||
同步备份功能,支持备份本地文件到云盘,备份云盘文件到本地两种模式。支持JavaScript插件对备份文件进行过滤。
|
||||
指定本地目录和对应的一个网盘目录,以备份文件。网盘目录必须和本地目录独占使用,不要用作其他用途,不然备份可能会有问题。
|
||||
@@ -450,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,样例如下:
|
||||
|
||||
@@ -518,14 +607,23 @@ drive - 网盘,支持:backup(备份盘), resource(资源盘)
|
||||
|
||||
sync.sh脚本,内容如下
|
||||
```
|
||||
# 请更改成你自己的目录
|
||||
cd /path/to/aliyunpan/folder
|
||||
#!/bin/bash
|
||||
|
||||
chmod +x ./aliyunpan
|
||||
# 是否开启调试日志
|
||||
export ALIYUNPAN_VERBOSE=0
|
||||
# (可选)配置目录的绝对路径(请更改成你自己的目录)
|
||||
export ALIYUNPAN_CONFIG_DIR=/Users/tickstep/Applications/adrive/config
|
||||
# aliyunpan程序所在的绝对路径(请更改成你自己的目录)
|
||||
export ALIYUNPAN_BIN=/Users/tickstep/Applications/adrive/aliyunpan
|
||||
|
||||
# 本地目录(请更改成你自己的目录)
|
||||
LOCAL_DIR="/tickstep/Documents/设计文档"
|
||||
# 网盘目录(请更改成你自己的目录)
|
||||
PAN_DIR="/备份盘/我的文档"
|
||||
|
||||
# 指定配置参数并进行启动
|
||||
# 支持的模式:upload(备份本地文件到云盘),download(备份云盘文件到本地)
|
||||
./aliyunpan sync start -ldir "/tickstep/Documents/设计文档" -pdir "/备份盘/我的文档" -mode "upload" -drive "backup"
|
||||
"$ALIYUNPAN_BIN" sync start -ldir "$LOCAL_DIR" -pdir "$PAN_DIR" -mode "upload" -drive "backup"
|
||||
```
|
||||
|
||||
增加脚本执行权限
|
||||
@@ -643,512 +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.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)
|
||||
|
||||
## 显示和修改程序配置项
|
||||
```
|
||||
@@ -1194,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");
|
||||
}
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
module github.com/tickstep/aliyunpan
|
||||
|
||||
go 1.20
|
||||
go 1.22
|
||||
|
||||
require (
|
||||
github.com/GeertJohan/go.incremental v1.0.0
|
||||
@@ -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.2
|
||||
github.com/tickstep/aliyunpan-api v0.2.9
|
||||
github.com/tickstep/bolt v1.3.4
|
||||
github.com/tickstep/library-go v0.1.2
|
||||
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,12 +101,12 @@ 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.2 h1:u1DS6wAg5dZ1595Q55iNZ87/M5cQ21b9/9B5twl9GJo=
|
||||
github.com/tickstep/aliyunpan-api v0.2.2/go.mod h1:uMYRMWJO8mEAW6nRkw/QnathYFzQ8AL8kKO/cdMt058=
|
||||
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.2 h1:iewbEZBl5+aWeL9zcjVHVC4BXET4+9gwc52fuDC8MXQ=
|
||||
github.com/tickstep/library-go v0.1.2/go.mod h1:uAHeNOIpoywCzlaeLrWmmRSupn03m9kJVZKOEmuarmA=
|
||||
github.com/tickstep/library-go v0.1.3 h1:OUj6nkimTsqhHXh5s+PbmHq5hR0m739Y1tu9QpfA2ng=
|
||||
github.com/tickstep/library-go v0.1.3/go.mod h1:uAHeNOIpoywCzlaeLrWmmRSupn03m9kJVZKOEmuarmA=
|
||||
github.com/urfave/cli v1.21.1-0.20190817182405-23c83030263f h1:xKDKjIsL76VUyHcA0G4Qe1cIAUB/nrq6Pt8D411bd1g=
|
||||
github.com/urfave/cli v1.21.1-0.20190817182405-23c83030263f/go.mod h1:qXyCeJubPqsgeiLd3kvHOGHHSrQcNdjZ2ScXIcVZK/I=
|
||||
golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
@@ -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=
|
||||
|
||||
+137
-443
@@ -14,46 +14,36 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan_web"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/file/downloader"
|
||||
"github.com/tickstep/aliyunpan/internal/functions/pandownload"
|
||||
"github.com/tickstep/aliyunpan/internal/global"
|
||||
"github.com/tickstep/aliyunpan/internal/taskframework"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/aliyunpan/library/requester/transfer"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/library-go/requester"
|
||||
"github.com/tickstep/library-go/requester/rio/speeds"
|
||||
"github.com/urfave/cli"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
AlbumFileCategoryOption string
|
||||
)
|
||||
|
||||
var (
|
||||
ImageOnlyOption AlbumFileCategoryOption = "image"
|
||||
VideoOnlyOption AlbumFileCategoryOption = "video"
|
||||
ImageVideoOnlyOption AlbumFileCategoryOption = "image_video"
|
||||
AllFileOption AlbumFileCategoryOption = "none"
|
||||
)
|
||||
|
||||
func CmdAlbum() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "album",
|
||||
Aliases: []string{"abm"},
|
||||
Usage: "个人相册(Beta)",
|
||||
Usage: "共享相册",
|
||||
UsageText: cmder.App().Name + " album",
|
||||
Category: "阿里云盘",
|
||||
Before: ReloadConfigFunc,
|
||||
@@ -66,12 +56,11 @@ func CmdAlbum() cli.Command {
|
||||
{
|
||||
Name: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Usage: "展示相簿列表",
|
||||
Usage: "展示共享相簿列表",
|
||||
UsageText: cmder.App().Name + " album list",
|
||||
Description: `
|
||||
示例:
|
||||
|
||||
展示相簿列表
|
||||
展示共享相簿列表
|
||||
aliyunpan album ls
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
@@ -79,94 +68,7 @@ func CmdAlbum() cli.Command {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
RunAlbumList()
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
{
|
||||
Name: "new",
|
||||
Aliases: []string{""},
|
||||
Usage: "创建相簿",
|
||||
UsageText: cmder.App().Name + " album new",
|
||||
Description: `
|
||||
示例:
|
||||
|
||||
新建相簿,名称为:我的相簿2022
|
||||
aliyunpan album new "我的相簿2022"
|
||||
|
||||
新建相簿,名称为:我的相簿2022,描述为:存放2022所有文件
|
||||
aliyunpan album new "我的相簿2022" "存放2022所有文件"
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
RunAlbumCreate(c.Args().Get(0), c.Args().Get(1))
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
{
|
||||
Name: "rm",
|
||||
Aliases: []string{""},
|
||||
Usage: "删除相簿",
|
||||
UsageText: cmder.App().Name + " album rm",
|
||||
Description: `
|
||||
删除相簿,同名的相簿只会删除第一个符合条件的
|
||||
示例:
|
||||
|
||||
删除名称为"我的相簿2022"的相簿
|
||||
aliyunpan album rm "我的相簿2022"
|
||||
|
||||
删除名称为"我的相簿2022-1" 和 "我的相簿2022-2"的相簿
|
||||
aliyunpan album rm "我的相簿2022-1" "我的相簿2022-2"
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
RunAlbumDelete(c.Args())
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
{
|
||||
Name: "rename",
|
||||
Aliases: []string{""},
|
||||
Usage: "重命名相簿",
|
||||
UsageText: cmder.App().Name + " album rename",
|
||||
Description: `
|
||||
重命名相簿,同名的相簿只会修改第一个符合条件的
|
||||
示例:
|
||||
|
||||
重命名相簿"我的相簿2022"为新的名称"我的相簿2022-new"
|
||||
aliyunpan album rename "我的相簿2022" "我的相簿2022-new"
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
RunAlbumRename(c.Args().Get(0), c.Args().Get(1))
|
||||
RunShareAlbumList()
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
@@ -188,76 +90,7 @@ func CmdAlbum() cli.Command {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
RunAlbumListFile(c.Args().Get(0))
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
{
|
||||
Name: "rm-file",
|
||||
Aliases: []string{"rf"},
|
||||
Usage: "移除相簿中的文件",
|
||||
UsageText: cmder.App().Name + " album rm-file",
|
||||
Description: `
|
||||
移除相簿中的文件,同名的相簿只会移除第一个符合条件的
|
||||
示例:
|
||||
|
||||
移除相簿 "我的相簿2022" 中的文件 1.png 2.png
|
||||
aliyunpan album rm-file 我的相簿2022 1.png 2.png
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
subArgs := c.Args()
|
||||
if len(subArgs) < 2 {
|
||||
fmt.Println("请指定移除的文件")
|
||||
return nil
|
||||
}
|
||||
RunAlbumRmFile(subArgs[0], subArgs[1:])
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
{
|
||||
Name: "add-file",
|
||||
Aliases: []string{"af"},
|
||||
Usage: "增加(文件/相册)网盘文件到相簿中",
|
||||
UsageText: cmder.App().Name + " album add-file",
|
||||
Description: `
|
||||
增加文件到相簿中
|
||||
示例:
|
||||
|
||||
增加当前目录下的 1.png 2.png 文件到相簿 "我的相簿2022" 中
|
||||
aliyunpan album add-file 我的相簿2022 1.png 2.png
|
||||
|
||||
增加当前目录下的 myFolder 文件夹下所有文件到相簿 "我的相簿2022" 中
|
||||
aliyunpan album add-file 我的相簿2022 myFolder
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
subArgs := c.Args()
|
||||
if len(subArgs) < 2 {
|
||||
fmt.Println("请指定增加的文件")
|
||||
return nil
|
||||
}
|
||||
RunAlbumAddFile(subArgs[0], subArgs[1:], ImageVideoOnlyOption)
|
||||
RunShareAlbumListFile(c.Args().Get(0))
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
@@ -273,17 +106,12 @@ func CmdAlbum() cli.Command {
|
||||
|
||||
下载相簿 "我的相簿2022" 里面的所有文件
|
||||
aliyunpan album download-file 我的相簿2022
|
||||
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
subArgs := c.Args()
|
||||
if len(subArgs) == 0 {
|
||||
fmt.Println("请指定下载的相簿名称")
|
||||
@@ -304,6 +132,7 @@ func CmdAlbum() cli.Command {
|
||||
IsOverwrite: c.Bool("ow"),
|
||||
SaveTo: saveTo,
|
||||
Parallel: 0,
|
||||
SliceParallel: 0,
|
||||
Load: 0,
|
||||
MaxRetry: pandownload.DefaultDownloadMaxRetry,
|
||||
NoCheck: false,
|
||||
@@ -312,7 +141,7 @@ func CmdAlbum() cli.Command {
|
||||
ExcludeNames: []string{},
|
||||
}
|
||||
|
||||
RunAlbumDownloadFile(c.Args(), do)
|
||||
RunShareAlbumDownloadFile(c.Args(), do)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
@@ -334,76 +163,25 @@ func CmdAlbum() cli.Command {
|
||||
}
|
||||
}
|
||||
|
||||
func RunAlbumList() {
|
||||
func RunShareAlbumList() {
|
||||
activeUser := GetActiveUser()
|
||||
records, err := activeUser.PanClient().WebapiPanClient().AlbumListGetAll(&aliyunpan_web.AlbumListParam{})
|
||||
records, err := activeUser.PanClient().OpenapiPanClient().ShareAlbumListGetAll()
|
||||
if err != nil {
|
||||
fmt.Printf("获取相簿列表失败: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
tb.SetHeader([]string{"#", "ALBUM_ID", "名称", "文件数量", "创建日期", "修改日期"})
|
||||
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_DEFAULT})
|
||||
tb.SetHeader([]string{"#", "ALBUM_ID", "名称", "更新日期", "创建日期"})
|
||||
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_DEFAULT})
|
||||
for k, record := range records {
|
||||
tb.Append([]string{strconv.Itoa(k + 1), record.AlbumId, record.Name, strconv.Itoa(record.FileCount),
|
||||
record.CreatedAtStr(), record.UpdatedAtStr()})
|
||||
tb.Append([]string{strconv.Itoa(k + 1), record.AlbumId, record.Name, record.UpdatedAtStr(), record.CreatedAtStr()})
|
||||
}
|
||||
tb.Render()
|
||||
}
|
||||
|
||||
func RunAlbumCreate(name, description string) {
|
||||
if name == "" {
|
||||
fmt.Printf("相簿名称不能为空\n")
|
||||
return
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
_, err := activeUser.PanClient().WebapiPanClient().AlbumCreate(&aliyunpan_web.AlbumCreateParam{
|
||||
Name: name,
|
||||
Description: description,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("创建相簿失败: %s\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("创建相簿成功: %s\n", name)
|
||||
}
|
||||
|
||||
func RunAlbumDelete(nameList []string) {
|
||||
if len(nameList) == 0 {
|
||||
fmt.Printf("相簿名称不能为空\n")
|
||||
return
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
records, err := activeUser.PanClient().WebapiPanClient().AlbumListGetAll(&aliyunpan_web.AlbumListParam{})
|
||||
if err != nil {
|
||||
fmt.Printf("获取相簿列表失败: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, record := range records {
|
||||
for i, name := range nameList {
|
||||
if name == record.Name {
|
||||
nameList = append(nameList[:i], nameList[i+1:]...)
|
||||
_, err := activeUser.PanClient().WebapiPanClient().AlbumDelete(&aliyunpan_web.AlbumDeleteParam{
|
||||
AlbumId: record.AlbumId,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("删除相簿失败: %s\n", name)
|
||||
return
|
||||
} else {
|
||||
fmt.Printf("删除相簿成功: %s\n", name)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getAlbumFromName(activeUser *config.PanUser, name string) *aliyunpan_web.AlbumEntity {
|
||||
records, err := activeUser.PanClient().WebapiPanClient().AlbumListGetAll(&aliyunpan_web.AlbumListParam{})
|
||||
func getShareAlbumFromName(activeUser *config.PanUser, name string) *aliyunpan.AlbumEntity {
|
||||
records, err := activeUser.PanClient().OpenapiPanClient().ShareAlbumListGetAll()
|
||||
if err != nil {
|
||||
fmt.Printf("获取相簿列表失败: %s\n", err)
|
||||
return nil
|
||||
@@ -417,48 +195,21 @@ func getAlbumFromName(activeUser *config.PanUser, name string) *aliyunpan_web.Al
|
||||
return nil
|
||||
}
|
||||
|
||||
func RunAlbumRename(name, newName string) {
|
||||
if len(name) == 0 {
|
||||
fmt.Printf("相簿名称不能为空\n")
|
||||
return
|
||||
}
|
||||
if len(newName) == 0 {
|
||||
fmt.Printf("相簿名称不能为空\n")
|
||||
return
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
record := getAlbumFromName(activeUser, name)
|
||||
if record == nil {
|
||||
return
|
||||
}
|
||||
_, err := activeUser.PanClient().WebapiPanClient().AlbumEdit(&aliyunpan_web.AlbumEditParam{
|
||||
AlbumId: record.AlbumId,
|
||||
Description: record.Description,
|
||||
Name: newName,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("重命名相簿失败: %s\n", name)
|
||||
return
|
||||
} else {
|
||||
fmt.Printf("重命名相簿成功: %s -> %s\n", name, newName)
|
||||
}
|
||||
}
|
||||
|
||||
func RunAlbumListFile(name string) {
|
||||
func RunShareAlbumListFile(name string) {
|
||||
if len(name) == 0 {
|
||||
fmt.Printf("相簿名称不能为空\n")
|
||||
return
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
record := getAlbumFromName(activeUser, name)
|
||||
record := getShareAlbumFromName(activeUser, name)
|
||||
if record == nil {
|
||||
return
|
||||
}
|
||||
|
||||
fileList, er := activeUser.PanClient().WebapiPanClient().AlbumListFileGetAll(&aliyunpan_web.AlbumListFileParam{
|
||||
fileList, er := activeUser.PanClient().OpenapiPanClient().ShareAlbumListFileGetAll(&aliyunpan.ShareAlbumListFileParam{
|
||||
AlbumId: record.AlbumId,
|
||||
Limit: 100,
|
||||
})
|
||||
if er != nil {
|
||||
fmt.Printf("获取相簿文件列表失败:%s\n", er)
|
||||
@@ -467,174 +218,13 @@ func RunAlbumListFile(name string) {
|
||||
renderTable(opLs, false, "", fileList)
|
||||
}
|
||||
|
||||
func RunAlbumRmFile(name string, nameList []string) {
|
||||
if len(name) == 0 {
|
||||
fmt.Printf("相簿名称不能为空\n")
|
||||
return
|
||||
}
|
||||
if len(nameList) == 0 {
|
||||
fmt.Printf("指定文件不能为空\n")
|
||||
return
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
album := getAlbumFromName(activeUser, name)
|
||||
if album == nil {
|
||||
return
|
||||
}
|
||||
|
||||
fileList, er := activeUser.PanClient().WebapiPanClient().AlbumListFileGetAll(&aliyunpan_web.AlbumListFileParam{
|
||||
AlbumId: album.AlbumId,
|
||||
})
|
||||
if er != nil {
|
||||
fmt.Printf("获取相簿文件列表失败:%s\n", er)
|
||||
return
|
||||
}
|
||||
param := &aliyunpan_web.AlbumDeleteFileParam{
|
||||
AlbumId: album.AlbumId,
|
||||
DriveFileList: []aliyunpan.FileBatchActionParam{},
|
||||
}
|
||||
for _, file := range fileList {
|
||||
if len(nameList) == 0 {
|
||||
break
|
||||
}
|
||||
for i, name := range nameList {
|
||||
if name == file.FileName {
|
||||
nameList = append(nameList[:i], nameList[i+1:]...)
|
||||
param.AddFileItem(file.DriveId, file.FileId)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1-500 范围
|
||||
if len(param.DriveFileList) == 0 {
|
||||
fmt.Printf("没有符合的文件\n")
|
||||
return
|
||||
}
|
||||
// delete file
|
||||
_, e := activeUser.PanClient().WebapiPanClient().AlbumDeleteFile(param)
|
||||
if e != nil {
|
||||
fmt.Printf("删除相簿文件失败:%s\n", e)
|
||||
return
|
||||
}
|
||||
fmt.Printf("删除相簿文件成功:%s\n", name)
|
||||
}
|
||||
|
||||
// RunAlbumAddFile 增加网盘文件到相簿
|
||||
func RunAlbumAddFile(albumName string, filePathList []string, filterOption AlbumFileCategoryOption) {
|
||||
activeUser := GetActiveUser()
|
||||
|
||||
if albumName == "" {
|
||||
fmt.Printf("必须指定相簿名称\n")
|
||||
return
|
||||
}
|
||||
album := getAlbumFromName(activeUser, albumName)
|
||||
if album == nil {
|
||||
fmt.Printf("相簿不存在\n")
|
||||
return
|
||||
}
|
||||
|
||||
paths, err := makePathAbsolute(activeUser.ActiveDriveId, filePathList...)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
if len(paths) == 0 {
|
||||
fmt.Printf("没有有效的文件\n")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("正在获取增加的文件信息,该操作可能会非常耗费时间,请耐心等待...\n")
|
||||
param := &aliyunpan_web.AlbumAddFileParam{
|
||||
AlbumId: album.AlbumId,
|
||||
DriveFileList: []aliyunpan.FileBatchActionParam{},
|
||||
}
|
||||
for k := range paths {
|
||||
filePath := paths[k]
|
||||
fileInfo, apierr := activeUser.PanClient().WebapiPanClient().FileInfoByPath(activeUser.ActiveDriveId, filePath)
|
||||
if apierr != nil {
|
||||
fmt.Printf("获取文件信息失败: %s\n", filePath)
|
||||
continue
|
||||
}
|
||||
if fileInfo.IsFile() {
|
||||
// file
|
||||
if isFileMatchCondition(fileInfo, filterOption) {
|
||||
param.AddFileItem(fileInfo.DriveId, fileInfo.FileId)
|
||||
}
|
||||
} else {
|
||||
// folder
|
||||
activeUser.PanClient().WebapiPanClient().FilesDirectoriesRecurseList(activeUser.ActiveDriveId, fileInfo.Path, func(depth int, _ string, fd *aliyunpan.FileEntity, apiError *apierror.ApiError) bool {
|
||||
if apiError != nil {
|
||||
logger.Verbosef("%s\n", apiError)
|
||||
return true
|
||||
}
|
||||
if !fd.IsFolder() {
|
||||
if isFileMatchCondition(fd, filterOption) {
|
||||
param.AddFileItem(fd.DriveId, fd.FileId)
|
||||
}
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
return true
|
||||
})
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
if len(param.DriveFileList) == 0 {
|
||||
fmt.Printf("没有符合的文件\n")
|
||||
return
|
||||
}
|
||||
// add file
|
||||
_, e := activeUser.PanClient().WebapiPanClient().AlbumAddFile(param)
|
||||
if e != nil {
|
||||
fmt.Printf("增加相簿文件失败:%s\n", e)
|
||||
return
|
||||
}
|
||||
fmt.Printf("增加相簿文件成功:%s\n", albumName)
|
||||
}
|
||||
|
||||
func isFileMatchCondition(fileInfo *aliyunpan.FileEntity, filterOption AlbumFileCategoryOption) bool {
|
||||
if fileInfo == nil {
|
||||
return false
|
||||
}
|
||||
if filterOption == ImageOnlyOption {
|
||||
return fileInfo.Category == "image"
|
||||
} else if filterOption == VideoOnlyOption {
|
||||
return fileInfo.Category == "video"
|
||||
} else if filterOption == ImageVideoOnlyOption {
|
||||
return fileInfo.Category == "image" || fileInfo.Category == "video"
|
||||
} else if filterOption == AllFileOption {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
||||
func RunShareAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
||||
if len(albumNames) == 0 {
|
||||
fmt.Printf("相簿名称不能为空\n")
|
||||
fmt.Printf("请指定相簿名称\n")
|
||||
return
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
activeUser.PanClient().WebapiPanClient().EnableCache()
|
||||
activeUser.PanClient().WebapiPanClient().ClearCache()
|
||||
defer activeUser.PanClient().WebapiPanClient().DisableCache()
|
||||
// pan token expired checker
|
||||
continueFlag := int32(0)
|
||||
atomic.StoreInt32(&continueFlag, 0)
|
||||
defer func() {
|
||||
atomic.StoreInt32(&continueFlag, 1)
|
||||
}()
|
||||
go func(flag *int32) {
|
||||
for atomic.LoadInt32(flag) == 0 {
|
||||
time.Sleep(time.Duration(1) * time.Minute)
|
||||
if RefreshWebTokenInNeed(activeUser, config.Config.DeviceName) {
|
||||
logger.Verboseln("update access token for download task")
|
||||
}
|
||||
}
|
||||
}(&continueFlag)
|
||||
|
||||
if options == nil {
|
||||
options = &DownloadOptions{}
|
||||
}
|
||||
@@ -668,6 +258,16 @@ func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
||||
if options.Parallel > config.MaxFileDownloadParallelNum {
|
||||
options.Parallel = config.MaxFileDownloadParallelNum
|
||||
}
|
||||
// 设置单个文件下载分片线程数
|
||||
if options.SliceParallel < 1 { // 用户没有主动设置,则使用下面自动配置的策略
|
||||
if options.Parallel > 1 {
|
||||
// 如果是多文件下载,则一个文件只能同时使用1个线程
|
||||
options.SliceParallel = 1
|
||||
} else {
|
||||
// 如果是单文件下载,则可以使用3个最大线程
|
||||
options.SliceParallel = 3
|
||||
}
|
||||
}
|
||||
|
||||
// 保存文件的本地根文件夹
|
||||
originSaveRootPath := ""
|
||||
@@ -693,6 +293,7 @@ func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
||||
panClient = activeUser.PanClient()
|
||||
)
|
||||
cfg.MaxParallel = options.Parallel
|
||||
cfg.SliceParallel = options.SliceParallel
|
||||
|
||||
var (
|
||||
executor = taskframework.TaskExecutor{
|
||||
@@ -707,14 +308,26 @@ func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
||||
globalSpeedsStat := &speeds.Speeds{}
|
||||
|
||||
// 处理队列
|
||||
allShareAlbumList, err := activeUser.PanClient().OpenapiPanClient().ShareAlbumListGetAll()
|
||||
if err != nil {
|
||||
fmt.Printf("获取相簿列表失败: %s\n", err)
|
||||
return
|
||||
}
|
||||
for k := range albumNames {
|
||||
record := getAlbumFromName(activeUser, albumNames[k])
|
||||
var record *aliyunpan.AlbumEntity
|
||||
for _, album := range allShareAlbumList {
|
||||
if album.Name == albumNames[k] {
|
||||
record = album
|
||||
break
|
||||
}
|
||||
}
|
||||
if record == nil {
|
||||
continue
|
||||
}
|
||||
// 获取相簿下的所有文件
|
||||
fileList, er := activeUser.PanClient().WebapiPanClient().AlbumListFileGetAll(&aliyunpan_web.AlbumListFileParam{
|
||||
fileList, er := activeUser.PanClient().OpenapiPanClient().ShareAlbumListFileGetAll(&aliyunpan.ShareAlbumListFileParam{
|
||||
AlbumId: record.AlbumId,
|
||||
Limit: 100,
|
||||
})
|
||||
if er != nil {
|
||||
fmt.Printf("获取相簿文件出错,请稍后重试: %s\n", albumNames[k])
|
||||
@@ -724,9 +337,61 @@ func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
||||
fmt.Printf("相簿里面没有文件: %s\n", albumNames[k])
|
||||
continue
|
||||
}
|
||||
for _, f := range fileList {
|
||||
// 补全虚拟网盘路径,规则:/<相簿名称>/文件名称
|
||||
f.Path = "/" + albumNames[k] + "/" + f.FileName
|
||||
|
||||
// 相薄文件是没有子文件夹的,这个fileList就是该相册里面的全部文件了,所以不需要再遍历子文件夹了
|
||||
idx := 0
|
||||
for {
|
||||
if idx >= len(fileList) {
|
||||
break
|
||||
}
|
||||
f := fileList.Item(idx)
|
||||
idx += 1
|
||||
// 处理实况照片
|
||||
if f.IsAlbumLivePhotoFile() {
|
||||
// 如果是实况照片,则需要下载图片+视频两个文件
|
||||
// 获取下载链接
|
||||
durl, apierr := activeUser.PanClient().OpenapiPanClient().ShareAlbumGetFileDownloadUrl(&aliyunpan.ShareAlbumGetFileUrlParam{
|
||||
AlbumId: f.AlbumId,
|
||||
DriveId: f.DriveId,
|
||||
FileId: f.FileId,
|
||||
})
|
||||
if apierr != nil {
|
||||
logger.Verbosef("ERROR: get album file download url error: %s\n", f.FileId)
|
||||
fmt.Printf("\n下载照片失败: %s\n", f.FileName)
|
||||
continue
|
||||
}
|
||||
if durl.StreamsUrl != nil { // 实况图片(照片+视频)下载链接
|
||||
// 照片文件
|
||||
photoFile := cloneFileEntity(f)
|
||||
photoFileSize := int64(0)
|
||||
if durl.StreamsUrl.Heic != "" {
|
||||
beforeStr, _ := strings.CutSuffix(f.FileName, ".livp")
|
||||
photoFile.FileName = beforeStr + ".HEIC"
|
||||
photoFile.FileExtension = "heic"
|
||||
photoFileSize = getHttpDownloadFileSize(durl.StreamsUrl.Heic)
|
||||
} else if durl.StreamsUrl.Jpeg != "" {
|
||||
beforeStr, _ := strings.CutSuffix(f.FileName, ".livp")
|
||||
photoFile.FileName = beforeStr + ".JPG"
|
||||
photoFile.FileExtension = "jpg"
|
||||
photoFileSize = getHttpDownloadFileSize(durl.StreamsUrl.Jpeg)
|
||||
}
|
||||
photoFile.FileSize = photoFileSize
|
||||
fileList = append(fileList, photoFile)
|
||||
|
||||
// 视频文件
|
||||
videoFile := cloneFileEntity(f)
|
||||
videoFile.FileSize = f.FileSize - photoFileSize
|
||||
if durl.StreamsUrl.Mov != "" {
|
||||
beforeStr, _ := strings.CutSuffix(f.FileName, ".livp")
|
||||
videoFile.FileName = beforeStr + ".MOV"
|
||||
videoFile.FileExtension = "mov"
|
||||
}
|
||||
fileList = append(fileList, videoFile)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// 补全虚拟网盘路径,规则:/共享相册/<相簿名称>/文件名称
|
||||
f.Path = "/共享相册/" + albumNames[k] + "/" + f.FileName
|
||||
|
||||
// 生成下载项
|
||||
newCfg := *cfg
|
||||
@@ -734,22 +399,21 @@ func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
||||
Cfg: &newCfg, // 复制一份新的cfg
|
||||
PanClient: panClient,
|
||||
VerbosePrinter: panCommandVerbose,
|
||||
PrintFormat: downloadPrintFormat(options.Load),
|
||||
ParentTaskExecutor: &executor,
|
||||
DownloadStatistic: statistic,
|
||||
IsPrintStatus: options.IsPrintStatus,
|
||||
IsExecutedPermission: options.IsExecutedPermission,
|
||||
IsOverwrite: options.IsOverwrite,
|
||||
NoCheck: options.NoCheck,
|
||||
FilePanSource: global.AlbumSource,
|
||||
FilePanPath: f.Path,
|
||||
DriveId: f.DriveId, // 必须使用文件的DriveId,因为一个相簿的文件会来自多个网盘(资源库/备份盘)
|
||||
DriveId: f.DriveId, // 一个相簿的文件会来自多个网盘(资源库/备份盘)
|
||||
GlobalSpeedsStat: globalSpeedsStat,
|
||||
FileRecorder: nil,
|
||||
}
|
||||
|
||||
// TODO: 相册下载需要重构
|
||||
// 设置相簿文件信息
|
||||
//unit.SetFileInfo(pandownload.AlbumFileSource, f)
|
||||
unit.SetFileInfo(global.AlbumSource, f)
|
||||
|
||||
// 设置储存的路径
|
||||
if options.SaveTo != "" {
|
||||
@@ -785,3 +449,33 @@ func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
||||
tb.Render()
|
||||
}
|
||||
}
|
||||
|
||||
func cloneFileEntity(entity *aliyunpan.FileEntity) *aliyunpan.FileEntity {
|
||||
data, err := json.Marshal(entity) // 序列化原始对象
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
newEntity := &aliyunpan.FileEntity{}
|
||||
err = json.Unmarshal(data, newEntity) // 反序列化到新的对象中
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return newEntity
|
||||
}
|
||||
|
||||
func getHttpDownloadFileSize(fileUrl string) int64 {
|
||||
client := requester.NewHTTPClient()
|
||||
client.SetKeepAlive(true)
|
||||
client.SetTimeout(10 * time.Minute)
|
||||
// header
|
||||
headers := map[string]string{
|
||||
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
|
||||
"referer": "https://www.aliyundrive.com/",
|
||||
}
|
||||
resp, err := client.Req("GET", fileUrl, nil, headers)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
fileSize := resp.ContentLength
|
||||
return fileSize
|
||||
}
|
||||
|
||||
@@ -0,0 +1,787 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/olekukonko/tablewriter"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan_web"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/file/downloader"
|
||||
"github.com/tickstep/aliyunpan/internal/functions/pandownload"
|
||||
"github.com/tickstep/aliyunpan/internal/functions/panlogin"
|
||||
"github.com/tickstep/aliyunpan/internal/taskframework"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/aliyunpan/library/requester/transfer"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/library-go/requester/rio/speeds"
|
||||
"github.com/urfave/cli"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
type (
|
||||
AlbumFileCategoryOption string
|
||||
)
|
||||
|
||||
var (
|
||||
ImageOnlyOption AlbumFileCategoryOption = "image"
|
||||
VideoOnlyOption AlbumFileCategoryOption = "video"
|
||||
ImageVideoOnlyOption AlbumFileCategoryOption = "image_video"
|
||||
AllFileOption AlbumFileCategoryOption = "none"
|
||||
)
|
||||
|
||||
func CmdAlbumWeb() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "albumw",
|
||||
Usage: "个人相册",
|
||||
UsageText: cmder.App().Name + " albumw",
|
||||
Category: "阿里云盘",
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
},
|
||||
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "list",
|
||||
Aliases: []string{"ls"},
|
||||
Usage: "展示相簿列表",
|
||||
UsageText: cmder.App().Name + " album list",
|
||||
Description: `
|
||||
示例:
|
||||
|
||||
展示相簿列表
|
||||
aliyunpan albumw ls
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
RunAlbumList()
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
{
|
||||
Name: "new",
|
||||
Aliases: []string{""},
|
||||
Usage: "创建相簿",
|
||||
UsageText: cmder.App().Name + " album new",
|
||||
Description: `
|
||||
示例:
|
||||
|
||||
新建相簿,名称为:我的相簿2022
|
||||
aliyunpan albumw new "我的相簿2022"
|
||||
|
||||
新建相簿,名称为:我的相簿2022,描述为:存放2022所有文件
|
||||
aliyunpan albumw new "我的相簿2022" "存放2022所有文件"
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
RunAlbumCreate(c.Args().Get(0), c.Args().Get(1))
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
{
|
||||
Name: "rm",
|
||||
Aliases: []string{""},
|
||||
Usage: "删除相簿",
|
||||
UsageText: cmder.App().Name + " album rm",
|
||||
Description: `
|
||||
删除相簿,同名的相簿只会删除第一个符合条件的
|
||||
示例:
|
||||
|
||||
删除名称为"我的相簿2022"的相簿
|
||||
aliyunpan albumw rm "我的相簿2022"
|
||||
|
||||
删除名称为"我的相簿2022-1" 和 "我的相簿2022-2"的相簿
|
||||
aliyunpan albumw rm "我的相簿2022-1" "我的相簿2022-2"
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
RunAlbumDelete(c.Args())
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
{
|
||||
Name: "rename",
|
||||
Aliases: []string{""},
|
||||
Usage: "重命名相簿",
|
||||
UsageText: cmder.App().Name + " album rename",
|
||||
Description: `
|
||||
重命名相簿,同名的相簿只会修改第一个符合条件的
|
||||
示例:
|
||||
|
||||
重命名相簿"我的相簿2022"为新的名称"我的相簿2022-new"
|
||||
aliyunpan albumw rename "我的相簿2022" "我的相簿2022-new"
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
RunAlbumRename(c.Args().Get(0), c.Args().Get(1))
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
{
|
||||
Name: "list-file",
|
||||
Aliases: []string{"lf"},
|
||||
Usage: "展示相簿中的文件",
|
||||
UsageText: cmder.App().Name + " album list-file",
|
||||
Description: `
|
||||
展示相簿中文件,同名的相簿只会展示第一个符合条件的
|
||||
示例:
|
||||
|
||||
展示相簿中文件"我的相簿2022"
|
||||
aliyunpan albumw list-file "我的相簿2022"
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
RunAlbumListFile(c.Args().Get(0))
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
{
|
||||
Name: "rm-file",
|
||||
Aliases: []string{"rf"},
|
||||
Usage: "移除相簿中的文件",
|
||||
UsageText: cmder.App().Name + " album rm-file",
|
||||
Description: `
|
||||
移除相簿中的文件,同名的相簿只会移除第一个符合条件的
|
||||
示例:
|
||||
|
||||
移除相簿 "我的相簿2022" 中的文件 1.png 2.png
|
||||
aliyunpan albumw rm-file 我的相簿2022 1.png 2.png
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
subArgs := c.Args()
|
||||
if len(subArgs) < 2 {
|
||||
fmt.Println("请指定移除的文件")
|
||||
return nil
|
||||
}
|
||||
RunAlbumRmFile(subArgs[0], subArgs[1:])
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
{
|
||||
Name: "add-file",
|
||||
Aliases: []string{"af"},
|
||||
Usage: "增加(文件/相册)网盘文件到相簿中",
|
||||
UsageText: cmder.App().Name + " album add-file",
|
||||
Description: `
|
||||
增加文件到相簿中
|
||||
示例:
|
||||
|
||||
增加当前目录下的 1.png 2.png 文件到相簿 "我的相簿2022" 中
|
||||
aliyunpan albumw add-file 我的相簿2022 1.png 2.png
|
||||
|
||||
增加当前目录下的 myFolder 文件夹下所有文件到相簿 "我的相簿2022" 中
|
||||
aliyunpan albumw add-file 我的相簿2022 myFolder
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
subArgs := c.Args()
|
||||
if len(subArgs) < 2 {
|
||||
fmt.Println("请指定增加的文件")
|
||||
return nil
|
||||
}
|
||||
RunAlbumAddFile(subArgs[0], subArgs[1:], ImageVideoOnlyOption)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
{
|
||||
Name: "download-file",
|
||||
Aliases: []string{"df"},
|
||||
Usage: "下载相簿中的所有文件到本地",
|
||||
UsageText: cmder.App().Name + " album download-file",
|
||||
Description: `
|
||||
下载相簿中的所有文件
|
||||
示例:
|
||||
|
||||
下载相簿 "我的相簿2022" 里面的所有文件
|
||||
aliyunpan albumw download-file 我的相簿2022
|
||||
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
subArgs := c.Args()
|
||||
if len(subArgs) == 0 {
|
||||
fmt.Println("请指定下载的相簿名称")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 处理saveTo
|
||||
var (
|
||||
saveTo string
|
||||
)
|
||||
if c.String("saveto") != "" {
|
||||
saveTo = filepath.Clean(c.String("saveto"))
|
||||
}
|
||||
|
||||
do := &DownloadOptions{
|
||||
IsPrintStatus: false,
|
||||
IsExecutedPermission: false,
|
||||
IsOverwrite: c.Bool("ow"),
|
||||
SaveTo: saveTo,
|
||||
Parallel: 0,
|
||||
Load: 0,
|
||||
MaxRetry: pandownload.DefaultDownloadMaxRetry,
|
||||
NoCheck: false,
|
||||
ShowProgress: !c.Bool("np"),
|
||||
DriveId: parseDriveId(c),
|
||||
ExcludeNames: []string{},
|
||||
}
|
||||
|
||||
RunAlbumDownloadFile(c.Args(), do)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "ow",
|
||||
Usage: "overwrite, 覆盖已存在的文件",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "saveto",
|
||||
Usage: "将下载的文件直接保存到指定的目录",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "np",
|
||||
Usage: "no progress 不展示下载进度条",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func RunAlbumList() {
|
||||
activeUser := GetActiveUser()
|
||||
records, err := activeUser.PanClient().WebapiPanClient().AlbumListGetAll(&aliyunpan_web.AlbumListParam{})
|
||||
if err != nil {
|
||||
fmt.Printf("获取相簿列表失败: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
tb.SetHeader([]string{"#", "ALBUM_ID", "名称", "文件数量", "创建日期", "修改日期"})
|
||||
tb.SetColumnAlignment([]int{tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_CENTER, tablewriter.ALIGN_DEFAULT, tablewriter.ALIGN_DEFAULT})
|
||||
for k, record := range records {
|
||||
tb.Append([]string{strconv.Itoa(k + 1), record.AlbumId, record.Name, strconv.Itoa(record.FileCount),
|
||||
record.CreatedAtStr(), record.UpdatedAtStr()})
|
||||
}
|
||||
tb.Render()
|
||||
}
|
||||
|
||||
func RunAlbumCreate(name, description string) {
|
||||
if name == "" {
|
||||
fmt.Printf("相簿名称不能为空\n")
|
||||
return
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
_, err := activeUser.PanClient().WebapiPanClient().AlbumCreate(&aliyunpan_web.AlbumCreateParam{
|
||||
Name: name,
|
||||
Description: description,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("创建相簿失败: %s\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("创建相簿成功: %s\n", name)
|
||||
}
|
||||
|
||||
func RunAlbumDelete(nameList []string) {
|
||||
if len(nameList) == 0 {
|
||||
fmt.Printf("相簿名称不能为空\n")
|
||||
return
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
records, err := activeUser.PanClient().WebapiPanClient().AlbumListGetAll(&aliyunpan_web.AlbumListParam{})
|
||||
if err != nil {
|
||||
fmt.Printf("获取相簿列表失败: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
for _, record := range records {
|
||||
for i, name := range nameList {
|
||||
if name == record.Name {
|
||||
nameList = append(nameList[:i], nameList[i+1:]...)
|
||||
_, err := activeUser.PanClient().WebapiPanClient().AlbumDelete(&aliyunpan_web.AlbumDeleteParam{
|
||||
AlbumId: record.AlbumId,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("删除相簿失败: %s\n", name)
|
||||
return
|
||||
} else {
|
||||
fmt.Printf("删除相簿成功: %s\n", name)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func getAlbumFromName(activeUser *config.PanUser, name string) *aliyunpan.AlbumEntity {
|
||||
records, err := activeUser.PanClient().WebapiPanClient().AlbumListGetAll(&aliyunpan_web.AlbumListParam{})
|
||||
if err != nil {
|
||||
fmt.Printf("获取相簿列表失败: %s\n", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, record := range records {
|
||||
if name == record.Name {
|
||||
return record
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func RunAlbumRename(name, newName string) {
|
||||
if len(name) == 0 {
|
||||
fmt.Printf("相簿名称不能为空\n")
|
||||
return
|
||||
}
|
||||
if len(newName) == 0 {
|
||||
fmt.Printf("相簿名称不能为空\n")
|
||||
return
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
record := getAlbumFromName(activeUser, name)
|
||||
if record == nil {
|
||||
return
|
||||
}
|
||||
_, err := activeUser.PanClient().WebapiPanClient().AlbumEdit(&aliyunpan_web.AlbumEditParam{
|
||||
AlbumId: record.AlbumId,
|
||||
Description: record.Description,
|
||||
Name: newName,
|
||||
})
|
||||
if err != nil {
|
||||
fmt.Printf("重命名相簿失败: %s\n", name)
|
||||
return
|
||||
} else {
|
||||
fmt.Printf("重命名相簿成功: %s -> %s\n", name, newName)
|
||||
}
|
||||
}
|
||||
|
||||
func RunAlbumListFile(name string) {
|
||||
if len(name) == 0 {
|
||||
fmt.Printf("相簿名称不能为空\n")
|
||||
return
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
record := getAlbumFromName(activeUser, name)
|
||||
if record == nil {
|
||||
return
|
||||
}
|
||||
|
||||
fileList, er := activeUser.PanClient().WebapiPanClient().AlbumListFileGetAll(&aliyunpan_web.AlbumListFileParam{
|
||||
AlbumId: record.AlbumId,
|
||||
})
|
||||
if er != nil {
|
||||
fmt.Printf("获取相簿文件列表失败:%s\n", er)
|
||||
return
|
||||
}
|
||||
renderTable(opLs, false, "", fileList)
|
||||
}
|
||||
|
||||
func RunAlbumRmFile(name string, nameList []string) {
|
||||
if len(name) == 0 {
|
||||
fmt.Printf("相簿名称不能为空\n")
|
||||
return
|
||||
}
|
||||
if len(nameList) == 0 {
|
||||
fmt.Printf("指定文件不能为空\n")
|
||||
return
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
album := getAlbumFromName(activeUser, name)
|
||||
if album == nil {
|
||||
return
|
||||
}
|
||||
|
||||
fileList, er := activeUser.PanClient().WebapiPanClient().AlbumListFileGetAll(&aliyunpan_web.AlbumListFileParam{
|
||||
AlbumId: album.AlbumId,
|
||||
})
|
||||
if er != nil {
|
||||
fmt.Printf("获取相簿文件列表失败:%s\n", er)
|
||||
return
|
||||
}
|
||||
param := &aliyunpan_web.AlbumDeleteFileParam{
|
||||
AlbumId: album.AlbumId,
|
||||
DriveFileList: []aliyunpan.FileBatchActionParam{},
|
||||
}
|
||||
for _, file := range fileList {
|
||||
if len(nameList) == 0 {
|
||||
break
|
||||
}
|
||||
for i, name := range nameList {
|
||||
if name == file.FileName {
|
||||
nameList = append(nameList[:i], nameList[i+1:]...)
|
||||
param.AddFileItem(file.DriveId, file.FileId)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 1-500 范围
|
||||
if len(param.DriveFileList) == 0 {
|
||||
fmt.Printf("没有符合的文件\n")
|
||||
return
|
||||
}
|
||||
// delete file
|
||||
_, e := activeUser.PanClient().WebapiPanClient().AlbumDeleteFile(param)
|
||||
if e != nil {
|
||||
fmt.Printf("删除相簿文件失败:%s\n", e)
|
||||
return
|
||||
}
|
||||
fmt.Printf("删除相簿文件成功:%s\n", name)
|
||||
}
|
||||
|
||||
// RunAlbumAddFile 增加网盘文件到相簿
|
||||
func RunAlbumAddFile(albumName string, filePathList []string, filterOption AlbumFileCategoryOption) {
|
||||
activeUser := GetActiveUser()
|
||||
|
||||
if albumName == "" {
|
||||
fmt.Printf("必须指定相簿名称\n")
|
||||
return
|
||||
}
|
||||
album := getAlbumFromName(activeUser, albumName)
|
||||
if album == nil {
|
||||
fmt.Printf("相簿不存在\n")
|
||||
return
|
||||
}
|
||||
|
||||
paths, err := makePathAbsolute(activeUser.ActiveDriveId, filePathList...)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
if len(paths) == 0 {
|
||||
fmt.Printf("没有有效的文件\n")
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("正在获取增加的文件信息,该操作可能会非常耗费时间,请耐心等待...\n")
|
||||
param := &aliyunpan_web.AlbumAddFileParam{
|
||||
AlbumId: album.AlbumId,
|
||||
DriveFileList: []aliyunpan.FileBatchActionParam{},
|
||||
}
|
||||
for k := range paths {
|
||||
filePath := paths[k]
|
||||
fileInfo, apierr := activeUser.PanClient().WebapiPanClient().FileInfoByPath(activeUser.ActiveDriveId, filePath)
|
||||
if apierr != nil {
|
||||
fmt.Printf("获取文件信息失败: %s\n", filePath)
|
||||
continue
|
||||
}
|
||||
if fileInfo.IsFile() {
|
||||
// file
|
||||
if isFileMatchCondition(fileInfo, filterOption) {
|
||||
param.AddFileItem(fileInfo.DriveId, fileInfo.FileId)
|
||||
}
|
||||
} else {
|
||||
// folder
|
||||
activeUser.PanClient().WebapiPanClient().FilesDirectoriesRecurseList(activeUser.ActiveDriveId, fileInfo.Path, func(depth int, _ string, fd *aliyunpan.FileEntity, apiError *apierror.ApiError) bool {
|
||||
if apiError != nil {
|
||||
logger.Verbosef("%s\n", apiError)
|
||||
return true
|
||||
}
|
||||
if !fd.IsFolder() {
|
||||
if isFileMatchCondition(fd, filterOption) {
|
||||
param.AddFileItem(fd.DriveId, fd.FileId)
|
||||
}
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
return true
|
||||
})
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
if len(param.DriveFileList) == 0 {
|
||||
fmt.Printf("没有符合的文件\n")
|
||||
return
|
||||
}
|
||||
// add file
|
||||
_, e := activeUser.PanClient().WebapiPanClient().AlbumAddFile(param)
|
||||
if e != nil {
|
||||
fmt.Printf("增加相簿文件失败:%s\n", e)
|
||||
return
|
||||
}
|
||||
fmt.Printf("增加相簿文件成功:%s\n", albumName)
|
||||
}
|
||||
|
||||
func isFileMatchCondition(fileInfo *aliyunpan.FileEntity, filterOption AlbumFileCategoryOption) bool {
|
||||
if fileInfo == nil {
|
||||
return false
|
||||
}
|
||||
if filterOption == ImageOnlyOption {
|
||||
return fileInfo.Category == "image"
|
||||
} else if filterOption == VideoOnlyOption {
|
||||
return fileInfo.Category == "video"
|
||||
} else if filterOption == ImageVideoOnlyOption {
|
||||
return fileInfo.Category == "image" || fileInfo.Category == "video"
|
||||
} else if filterOption == AllFileOption {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func RunAlbumDownloadFile(albumNames []string, options *DownloadOptions) {
|
||||
if len(albumNames) == 0 {
|
||||
fmt.Printf("相簿名称不能为空\n")
|
||||
return
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
activeUser.PanClient().WebapiPanClient().EnableCache()
|
||||
activeUser.PanClient().WebapiPanClient().ClearCache()
|
||||
defer activeUser.PanClient().WebapiPanClient().DisableCache()
|
||||
// pan token expired checker
|
||||
continueFlag := int32(0)
|
||||
atomic.StoreInt32(&continueFlag, 0)
|
||||
defer func() {
|
||||
atomic.StoreInt32(&continueFlag, 1)
|
||||
}()
|
||||
go func(flag *int32) {
|
||||
for atomic.LoadInt32(flag) == 0 {
|
||||
time.Sleep(time.Duration(1) * time.Minute)
|
||||
re := RefreshWebTokenInNeed(activeUser)
|
||||
if re.Code == panlogin.SUCCESS {
|
||||
logger.Verboseln("update access token for download task")
|
||||
}
|
||||
}
|
||||
}(&continueFlag)
|
||||
|
||||
if options == nil {
|
||||
options = &DownloadOptions{}
|
||||
}
|
||||
|
||||
if options.MaxRetry < 0 {
|
||||
options.MaxRetry = pandownload.DefaultDownloadMaxRetry
|
||||
}
|
||||
options.IsExecutedPermission = false
|
||||
|
||||
// 设置下载配置
|
||||
cfg := &downloader.Config{
|
||||
Mode: transfer.RangeGenMode_BlockSize,
|
||||
CacheSize: config.Config.CacheSize,
|
||||
BlockSize: MaxDownloadRangeSize,
|
||||
MaxRate: config.Config.MaxDownloadRate,
|
||||
InstanceStateStorageFormat: downloader.InstanceStateStorageFormatJSON,
|
||||
ShowProgress: options.ShowProgress,
|
||||
ExcludeNames: options.ExcludeNames,
|
||||
}
|
||||
if cfg.CacheSize == 0 {
|
||||
cfg.CacheSize = int(DownloadCacheSize)
|
||||
}
|
||||
|
||||
// 设置下载最大并发量
|
||||
if options.Parallel < 1 {
|
||||
options.Parallel = config.Config.MaxDownloadParallel
|
||||
if options.Parallel == 0 {
|
||||
options.Parallel = config.DefaultFileDownloadParallelNum
|
||||
}
|
||||
}
|
||||
if options.Parallel > config.MaxFileDownloadParallelNum {
|
||||
options.Parallel = config.MaxFileDownloadParallelNum
|
||||
}
|
||||
|
||||
// 保存文件的本地根文件夹
|
||||
originSaveRootPath := ""
|
||||
if options.SaveTo != "" {
|
||||
originSaveRootPath = options.SaveTo
|
||||
} else {
|
||||
// 使用默认的保存路径
|
||||
originSaveRootPath = GetActiveUser().GetSavePath("")
|
||||
}
|
||||
fi, err1 := os.Stat(originSaveRootPath)
|
||||
if err1 != nil && !os.IsExist(err1) {
|
||||
os.MkdirAll(originSaveRootPath, 0777) // 首先在本地创建目录
|
||||
} else {
|
||||
if !fi.IsDir() {
|
||||
fmt.Println("本地保存路径不是文件夹,请删除或者创建对应的文件夹:", originSaveRootPath)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\n[0] 当前文件下载最大并发量为: %d, 下载缓存为: %s\n\n", options.Parallel, converter.ConvertFileSize(int64(cfg.CacheSize), 2))
|
||||
|
||||
var (
|
||||
panClient = activeUser.PanClient()
|
||||
)
|
||||
cfg.MaxParallel = options.Parallel
|
||||
|
||||
var (
|
||||
executor = taskframework.TaskExecutor{
|
||||
IsFailedDeque: true, // 统计失败的列表
|
||||
}
|
||||
statistic = &pandownload.DownloadStatistic{}
|
||||
)
|
||||
// 配置执行器任务并发数,即同时下载文件并发数
|
||||
executor.SetParallel(cfg.MaxParallel)
|
||||
|
||||
// 全局速度统计
|
||||
globalSpeedsStat := &speeds.Speeds{}
|
||||
|
||||
// 处理队列
|
||||
for k := range albumNames {
|
||||
record := getAlbumFromName(activeUser, albumNames[k])
|
||||
if record == nil {
|
||||
continue
|
||||
}
|
||||
// 获取相簿下的所有文件
|
||||
fileList, er := activeUser.PanClient().WebapiPanClient().AlbumListFileGetAll(&aliyunpan_web.AlbumListFileParam{
|
||||
AlbumId: record.AlbumId,
|
||||
})
|
||||
if er != nil {
|
||||
fmt.Printf("获取相簿文件出错,请稍后重试: %s\n", albumNames[k])
|
||||
continue
|
||||
}
|
||||
if fileList == nil || len(fileList) == 0 {
|
||||
fmt.Printf("相簿里面没有文件: %s\n", albumNames[k])
|
||||
continue
|
||||
}
|
||||
for _, f := range fileList {
|
||||
// 补全虚拟网盘路径,规则:/<相簿名称>/文件名称
|
||||
f.Path = "/" + albumNames[k] + "/" + f.FileName
|
||||
|
||||
// 生成下载项
|
||||
newCfg := *cfg
|
||||
unit := pandownload.DownloadTaskUnit{
|
||||
Cfg: &newCfg, // 复制一份新的cfg
|
||||
PanClient: panClient,
|
||||
VerbosePrinter: panCommandVerbose,
|
||||
ParentTaskExecutor: &executor,
|
||||
DownloadStatistic: statistic,
|
||||
IsPrintStatus: options.IsPrintStatus,
|
||||
IsExecutedPermission: options.IsExecutedPermission,
|
||||
IsOverwrite: options.IsOverwrite,
|
||||
NoCheck: options.NoCheck,
|
||||
FilePanPath: f.Path,
|
||||
DriveId: f.DriveId, // 必须使用文件的DriveId,因为一个相簿的文件会来自多个网盘(资源库/备份盘)
|
||||
GlobalSpeedsStat: globalSpeedsStat,
|
||||
FileRecorder: nil,
|
||||
}
|
||||
|
||||
// TODO: 相册下载需要重构
|
||||
// 设置相簿文件信息
|
||||
//unit.SetFileInfo(pandownload.AlbumFileSource, f)
|
||||
|
||||
// 设置储存的路径
|
||||
if options.SaveTo != "" {
|
||||
unit.OriginSaveRootPath = options.SaveTo
|
||||
unit.SavePath = filepath.Join(options.SaveTo, f.Path)
|
||||
} else {
|
||||
// 使用默认的保存路径
|
||||
unit.OriginSaveRootPath = GetActiveUser().GetSavePath("")
|
||||
unit.SavePath = GetActiveUser().GetSavePath(f.Path)
|
||||
}
|
||||
info := executor.Append(&unit, options.MaxRetry)
|
||||
fmt.Printf("[%s] 加入下载队列: %s\n", info.Id(), f.Path)
|
||||
}
|
||||
}
|
||||
|
||||
// 开始计时
|
||||
statistic.StartTimer()
|
||||
|
||||
// 开始执行
|
||||
executor.Execute()
|
||||
|
||||
fmt.Printf("\n下载结束, 时间: %s, 数据总量: %s\n", utils.ConvertTime(statistic.Elapsed()), converter.ConvertFileSize(statistic.TotalSize(), 2))
|
||||
|
||||
// 输出失败的文件列表
|
||||
failedList := executor.FailedDeque()
|
||||
if failedList.Size() != 0 {
|
||||
fmt.Printf("以下文件下载失败: \n")
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
for e := failedList.Shift(); e != nil; e = failedList.Shift() {
|
||||
item := e.(*taskframework.TaskInfoItem)
|
||||
tb.Append([]string{item.Info.Id(), item.Unit.(*pandownload.DownloadTaskUnit).FilePanPath})
|
||||
}
|
||||
tb.Render()
|
||||
}
|
||||
}
|
||||
@@ -15,34 +15,40 @@ 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"
|
||||
"github.com/tickstep/aliyunpan/internal/file/downloader"
|
||||
"github.com/tickstep/aliyunpan/internal/functions/pandownload"
|
||||
"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
|
||||
@@ -50,6 +56,7 @@ type (
|
||||
DriveId string
|
||||
ExcludeNames []string // 排除的文件名,包括文件夹和文件。即这些文件/文件夹不进行下载,支持正则表达式
|
||||
IsMultiUserDownload bool // 是否启用多用户联合下载
|
||||
IsUseUIDashboard bool // 是否使用UI下载面板显示下载进度
|
||||
}
|
||||
|
||||
// LocateDownloadOption 获取下载链接可选参数
|
||||
@@ -130,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"),
|
||||
@@ -143,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{
|
||||
@@ -187,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: "下载失败最大重试次数",
|
||||
@@ -219,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()
|
||||
@@ -291,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
|
||||
}
|
||||
}
|
||||
|
||||
// 保存文件的本地根文件夹
|
||||
@@ -357,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()
|
||||
@@ -377,48 +375,80 @@ 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,
|
||||
IsExecutedPermission: options.IsExecutedPermission,
|
||||
IsOverwrite: options.IsOverwrite,
|
||||
NoCheck: options.NoCheck,
|
||||
FilePanSource: global.FileSource,
|
||||
FilePanPath: f.Path,
|
||||
DriveId: options.DriveId,
|
||||
GlobalSpeedsStat: globalSpeedsStat,
|
||||
FileRecorder: fileRecorder,
|
||||
UI: dashboard,
|
||||
}
|
||||
|
||||
// 设置储存的路径
|
||||
@@ -431,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))
|
||||
|
||||
// 输出失败的文件列表
|
||||
|
||||
@@ -132,10 +132,10 @@ func RunLogin() (ticketId string, openapiToken, webapiToken *config.PanClientTok
|
||||
loginUrl := &strings.Builder{}
|
||||
if global.IsSupportNoneOpenApiCommands {
|
||||
// 兼容以前的版本
|
||||
fmt.Fprintf(loginUrl, "https://openapi.alipan.com/oauth/authorize?client_id=%s&redirect_uri=https%%3A%%2F%%2Fapi.tickstep.com%%2Fauth%%2Ftickstep%%2Faliyunpan%%2Ftoken%%2Fopenapi%%2F%s%%2Fauth&scope=user:base,file:all:read,file:all:write",
|
||||
fmt.Fprintf(loginUrl, "https://openapi.alipan.com/oauth/authorize?client_id=%s&redirect_uri=https%%3A%%2F%%2Fapi.tickstep.com%%2Fauth%%2Ftickstep%%2Faliyunpan%%2Ftoken%%2Fopenapi%%2F%s%%2Fauth&scope=user:base,file:all:read,file:all:write,file:share:write,album:shared:read",
|
||||
config.Config.ClientId, ticketId)
|
||||
} else {
|
||||
fmt.Fprintf(loginUrl, "https://openapi.alipan.com/oauth/authorize?client_id=%s&redirect_uri=https%%3A%%2F%%2Fapi.tickstep.com%%2Fauth%%2Ftickstep%%2Faliyunpan%%2Ftoken%%2Fopenapi%%2F%s%%2Fauth2&scope=user:base,file:all:read,file:all:write",
|
||||
fmt.Fprintf(loginUrl, "https://openapi.alipan.com/oauth/authorize?client_id=%s&redirect_uri=https%%3A%%2F%%2Fapi.tickstep.com%%2Fauth%%2Ftickstep%%2Faliyunpan%%2Ftoken%%2Fopenapi%%2F%s%%2Fauth2&scope=user:base,file:all:read,file:all:write,file:share:write,album:shared:read",
|
||||
config.Config.ClientId, ticketId)
|
||||
}
|
||||
fmt.Printf("请在浏览器打开以下链接进行登录,链接有效时间为5分钟。\n注意:你需要进行一次授权一次扫码的两次登录。\n%s\n\n", loginUrl)
|
||||
|
||||
@@ -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("本次操作没有删除任何文件")
|
||||
}
|
||||
}
|
||||
|
||||
+24
-237
@@ -14,19 +14,14 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan_web"
|
||||
"github.com/tickstep/aliyunpan/cmder"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdtable"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/urfave/cli"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -74,10 +69,8 @@ func CmdShare() cli.Command {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
|
||||
// 有效期
|
||||
et := ""
|
||||
timeFlag := "0"
|
||||
if c.IsSet("time") {
|
||||
@@ -92,11 +85,16 @@ func CmdShare() cli.Command {
|
||||
et = ""
|
||||
}
|
||||
|
||||
// 密码
|
||||
sharePwd := ""
|
||||
if c.IsSet("sharePwd") {
|
||||
sharePwd = c.String("sharePwd")
|
||||
}
|
||||
if sharePwd == "" {
|
||||
sharePwd = RandomStr(4)
|
||||
}
|
||||
|
||||
// 模式
|
||||
modeFlag := "3"
|
||||
if c.IsSet("mode") {
|
||||
modeFlag = c.String("mode")
|
||||
@@ -108,7 +106,6 @@ func CmdShare() cli.Command {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if modeFlag == "1" {
|
||||
if sharePwd == "" {
|
||||
sharePwd = RandomStr(4)
|
||||
@@ -116,7 +113,8 @@ func CmdShare() cli.Command {
|
||||
} else {
|
||||
sharePwd = ""
|
||||
}
|
||||
RunShareSet(modeFlag, parseDriveId(c), c.Args(), et, sharePwd)
|
||||
|
||||
RunOpenShareSet(modeFlag, parseDriveId(c), c.Args(), et, sharePwd)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
@@ -142,97 +140,12 @@ func CmdShare() cli.Command {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "list",
|
||||
Aliases: []string{"l"},
|
||||
Usage: "列出已分享文件/目录",
|
||||
UsageText: cmder.App().Name + " share list",
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
RunShareList()
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
{
|
||||
Name: "cancel",
|
||||
Aliases: []string{"c"},
|
||||
Usage: "取消分享文件/目录",
|
||||
UsageText: cmder.App().Name + " share cancel <shareid_1> <shareid_2> ...",
|
||||
Description: `目前只支持通过分享id (shareid) 来取消分享.`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
if c.NArg() < 1 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
RunShareCancel(c.Args())
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "export",
|
||||
Usage: "导出分享记录保存到文件",
|
||||
UsageText: cmder.App().Name + " share export <csv file path>",
|
||||
Description: `
|
||||
导出分享记录,并保存到指定的文件。目前支持csv格式
|
||||
|
||||
示例:
|
||||
导出所有有效的分享并保存成文件
|
||||
aliyunpan share export "d:\myfoler\share_list.csv"
|
||||
|
||||
导出所有的分享并保存成文件
|
||||
aliyunpan share export -option 2 "d:\myfoler\share_list.csv"
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
if c.NArg() < 1 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
opt := c.String("option")
|
||||
if opt == "" {
|
||||
opt = "1"
|
||||
}
|
||||
filePath := c.Args()[0]
|
||||
RunShareExport(opt, filePath)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "option",
|
||||
Usage: "导出选项,1-有效分享 2-全部分享",
|
||||
Value: "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RunShareSet 执行分享
|
||||
func RunShareSet(modeFlag, driveId string, paths []string, expiredTime string, sharePwd string) {
|
||||
// RunOpenShareSet 执行分享
|
||||
func RunOpenShareSet(modeFlag, driveId string, paths []string, expiredTime string, sharePwd string) {
|
||||
if len(paths) <= 0 {
|
||||
fmt.Println("请指定文件路径")
|
||||
return
|
||||
@@ -267,9 +180,10 @@ func RunShareSet(modeFlag, driveId string, paths []string, expiredTime string, s
|
||||
return
|
||||
}
|
||||
|
||||
// 创建分享类型
|
||||
if modeFlag == "3" {
|
||||
// 快传
|
||||
r, err1 := panClient.WebapiPanClient().FastShareLinkCreate(aliyunpan_web.FastShareCreateParam{
|
||||
r, err1 := panClient.OpenapiPanClient().FastShareLinkCreate(aliyunpan.FastShareCreateParam{
|
||||
DriveId: driveId,
|
||||
FileIdList: fidList,
|
||||
})
|
||||
@@ -283,10 +197,10 @@ func RunShareSet(modeFlag, driveId string, paths []string, expiredTime string, s
|
||||
}
|
||||
|
||||
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.WebapiPanClient().ShareLinkCreate(aliyunpan_web.ShareCreateParam{
|
||||
r, err1 := panClient.OpenapiPanClient().ShareLinkCreate(aliyunpan.ShareCreateParam{
|
||||
DriveId: driveId,
|
||||
SharePwd: sharePwd,
|
||||
Expiration: expiredTime,
|
||||
@@ -301,143 +215,16 @@ func RunShareSet(modeFlag, driveId string, paths []string, expiredTime string, s
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("创建分享链接成功\n")
|
||||
if modeFlag == "1" {
|
||||
fmt.Printf("创建私密分享链接成功\n")
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RunShareList 执行列出分享列表
|
||||
func RunShareList() {
|
||||
activeUser := GetActiveUser()
|
||||
records, err := activeUser.PanClient().WebapiPanClient().ShareLinkList(activeUser.UserId)
|
||||
if err != nil {
|
||||
fmt.Printf("获取分享列表失败: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
tb.SetHeader([]string{"#", "ShARE_ID", "分享链接", "提取码", "文件名", "过期时间", "状态"})
|
||||
now := time.Now()
|
||||
for k, record := range records {
|
||||
et := "永久有效"
|
||||
if len(record.Expiration) > 0 {
|
||||
et = record.Expiration
|
||||
}
|
||||
status := "有效"
|
||||
if record.Status == "enabled" {
|
||||
if record.FirstFile == nil {
|
||||
status = "已删除"
|
||||
} else {
|
||||
cz := time.FixedZone("CST", 8*3600)
|
||||
if len(record.Expiration) > 0 {
|
||||
expiredTime, _ := time.ParseInLocation("2006-01-02 15:04:05", record.Expiration, cz)
|
||||
if expiredTime.Unix() < now.Unix() {
|
||||
status = "已过期"
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if record.Status == "forbidden" {
|
||||
status = "违规"
|
||||
}
|
||||
|
||||
tb.Append([]string{strconv.Itoa(k + 1), record.ShareId, record.ShareUrl, record.SharePwd,
|
||||
record.ShareName,
|
||||
//record.FileIdList[0],
|
||||
et,
|
||||
status})
|
||||
}
|
||||
tb.Render()
|
||||
}
|
||||
|
||||
// RunShareCancel 执行取消分享
|
||||
func RunShareCancel(shareIdList []string) {
|
||||
if len(shareIdList) == 0 {
|
||||
fmt.Printf("取消分享操作失败, 没有任何 shareid\n")
|
||||
return
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
r, err := activeUser.PanClient().WebapiPanClient().ShareLinkCancel(shareIdList)
|
||||
if err != nil {
|
||||
fmt.Printf("取消分享操作失败: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
if r != nil && len(r) > 0 {
|
||||
fmt.Printf("取消分享操作成功\n")
|
||||
} else {
|
||||
fmt.Printf("取消分享操作失败\n")
|
||||
}
|
||||
}
|
||||
|
||||
func RunShareExport(option, saveFilePath string) {
|
||||
activeUser := GetActiveUser()
|
||||
records, err := activeUser.PanClient().WebapiPanClient().ShareLinkList(activeUser.UserId)
|
||||
if err != nil {
|
||||
fmt.Printf("获取分享列表失败: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
columns := [][]string{{"序号", "分享ID", "分享链接", "提取码", "文件名", "过期时间", "状态"}}
|
||||
now := time.Now()
|
||||
idx := 1
|
||||
for _, record := range records {
|
||||
et := "永久有效"
|
||||
if len(record.Expiration) > 0 {
|
||||
et = record.Expiration
|
||||
}
|
||||
status := "有效"
|
||||
if record.FirstFile == nil {
|
||||
status = "已删除"
|
||||
if option == "1" {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
cz := time.FixedZone("CST", 8*3600)
|
||||
if len(record.Expiration) > 0 {
|
||||
expiredTime, _ := time.ParseInLocation("2006-01-02 15:04:05", record.Expiration, cz)
|
||||
if expiredTime.Unix() < now.Unix() {
|
||||
status = "已过期"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if option == "1" {
|
||||
if status == "已过期" || status == "已删除" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
line := []string{strconv.Itoa(idx), record.ShareId, record.ShareUrl, record.SharePwd, record.ShareName, et, status}
|
||||
idx += 1
|
||||
columns = append(columns, line)
|
||||
}
|
||||
|
||||
// save to file
|
||||
if ExportCsv(saveFilePath, columns) {
|
||||
fmt.Println("分享导出成功:", saveFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
func ExportCsv(savePath string, data [][]string) bool {
|
||||
folder := filepath.Dir(savePath)
|
||||
if _, err := os.Stat(folder); err != nil {
|
||||
if !os.IsExist(err) {
|
||||
os.MkdirAll(folder, os.ModePerm)
|
||||
}
|
||||
}
|
||||
fp, err := os.Create(savePath) // 创建文件句柄
|
||||
if err != nil {
|
||||
fmt.Println("创建文件["+savePath+"]失败,%v", err)
|
||||
return false
|
||||
}
|
||||
defer fp.Close()
|
||||
fp.WriteString("\xEF\xBB\xBF") // 写入UTF-8 BOM
|
||||
w := csv.NewWriter(fp) //创建一个新的写入文件流
|
||||
w.WriteAll(data)
|
||||
w.Flush()
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -0,0 +1,443 @@
|
||||
// Copyright (c) 2020 tickstep.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
package command
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"fmt"
|
||||
"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"
|
||||
"github.com/urfave/cli"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// web端分享接口
|
||||
func CmdShareWeb() cli.Command {
|
||||
return cli.Command{
|
||||
Name: "sharew",
|
||||
Usage: "分享文件/目录",
|
||||
UsageText: cmder.App().Name + " sharew",
|
||||
Category: "阿里云盘",
|
||||
Before: ReloadConfigFunc,
|
||||
Action: func(c *cli.Context) error {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
},
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "set",
|
||||
Aliases: []string{"s"},
|
||||
Usage: "设置分享文件/目录",
|
||||
UsageText: cmder.App().Name + " share set <文件/目录1> <文件/目录2> ...",
|
||||
Description: `
|
||||
示例:
|
||||
|
||||
创建文件 1.mp4 的分享链接
|
||||
aliyunpan share set -mode 1 1.mp4
|
||||
|
||||
创建 /我的视频/ 目录下所有mp4文件的分享链接,支持通配符
|
||||
aliyunpan share set -mode 1 /我的视频/*.mp4
|
||||
|
||||
创建文件 1.mp4 的分享链接,并指定分享密码为2333
|
||||
aliyunpan share set -mode 1 -sharePwd 2333 1.mp4
|
||||
|
||||
创建文件 1.mp4 的分享链接,并指定有效期为1天
|
||||
aliyunpan share set -mode 1 -time 1 1.mp4
|
||||
|
||||
创建文件 1.mp4 的快传链接
|
||||
aliyunpan share set 1.mp4
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if c.NArg() < 1 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
et := ""
|
||||
timeFlag := "0"
|
||||
if c.IsSet("time") {
|
||||
timeFlag = c.String("time")
|
||||
}
|
||||
now := time.Now()
|
||||
if timeFlag == "1" {
|
||||
et = now.Add(time.Duration(1) * time.Hour * 24).Format("2006-01-02 15:04:05")
|
||||
} else if timeFlag == "2" {
|
||||
et = now.Add(time.Duration(7) * time.Hour * 24).Format("2006-01-02 15:04:05")
|
||||
} else {
|
||||
et = ""
|
||||
}
|
||||
|
||||
sharePwd := ""
|
||||
if c.IsSet("sharePwd") {
|
||||
sharePwd = c.String("sharePwd")
|
||||
}
|
||||
|
||||
modeFlag := "3"
|
||||
if c.IsSet("mode") {
|
||||
modeFlag = c.String("mode")
|
||||
}
|
||||
if modeFlag == "1" || modeFlag == "2" {
|
||||
if config.Config.ActiveUser().ActiveDriveId != config.Config.ActiveUser().DriveList.GetResourceDriveId() {
|
||||
// 只有资源库才支持私有、公开分享
|
||||
fmt.Println("只有资源库才支持分享链接,其他请使用快传链接")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if modeFlag == "1" {
|
||||
if sharePwd == "" {
|
||||
sharePwd = RandomStr(4)
|
||||
}
|
||||
} else {
|
||||
sharePwd = ""
|
||||
}
|
||||
RunShareSet(modeFlag, parseDriveId(c), c.Args(), et, sharePwd)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "driveId",
|
||||
Usage: "网盘ID",
|
||||
Value: "",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "time",
|
||||
Usage: "有效期,0-永久,1-1天,2-7天",
|
||||
Value: "0",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "mode",
|
||||
Usage: "模式,1-私密分享,2-公开分享,3-快传",
|
||||
Value: "3",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "sharePwd",
|
||||
Usage: "自定义私密分享密码,4个字符,没有指定则随机生成",
|
||||
Value: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "list",
|
||||
Aliases: []string{"l"},
|
||||
Usage: "列出已分享文件/目录",
|
||||
UsageText: cmder.App().Name + " share list",
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
RunShareList()
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{},
|
||||
},
|
||||
{
|
||||
Name: "cancel",
|
||||
Aliases: []string{"c"},
|
||||
Usage: "取消分享文件/目录",
|
||||
UsageText: cmder.App().Name + " share cancel <shareid_1> <shareid_2> ...",
|
||||
Description: `目前只支持通过分享id (shareid) 来取消分享.`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
if c.NArg() < 1 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
RunShareCancel(c.Args())
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "export",
|
||||
Usage: "导出分享记录保存到文件",
|
||||
UsageText: cmder.App().Name + " share export <csv file path>",
|
||||
Description: `
|
||||
导出分享记录,并保存到指定的文件。目前支持csv格式
|
||||
|
||||
示例:
|
||||
导出所有有效的分享并保存成文件
|
||||
aliyunpan share export "d:\myfoler\share_list.csv"
|
||||
|
||||
导出所有的分享并保存成文件
|
||||
aliyunpan share export -option 2 "d:\myfoler\share_list.csv"
|
||||
`,
|
||||
Action: func(c *cli.Context) error {
|
||||
if config.Config.ActiveUser() == nil {
|
||||
fmt.Println("未登录账号")
|
||||
return nil
|
||||
}
|
||||
if config.Config.ActiveUser().PanClient().WebapiPanClient() == nil {
|
||||
fmt.Println("WEB客户端未登录,请登录后再使用该命令")
|
||||
return nil
|
||||
}
|
||||
if c.NArg() < 1 {
|
||||
cli.ShowCommandHelp(c, c.Command.Name)
|
||||
return nil
|
||||
}
|
||||
opt := c.String("option")
|
||||
if opt == "" {
|
||||
opt = "1"
|
||||
}
|
||||
filePath := c.Args()[0]
|
||||
RunShareExport(opt, filePath)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "option",
|
||||
Usage: "导出选项,1-有效分享 2-全部分享",
|
||||
Value: "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RunShareSet 执行分享
|
||||
func RunShareSet(modeFlag, driveId string, paths []string, expiredTime string, sharePwd string) {
|
||||
if len(paths) <= 0 {
|
||||
fmt.Println("请指定文件路径")
|
||||
return
|
||||
}
|
||||
activeUser := GetActiveUser()
|
||||
panClient := activeUser.PanClient()
|
||||
|
||||
allFileList := []*aliyunpan.FileEntity{}
|
||||
for idx := 0; idx < len(paths); idx++ {
|
||||
absolutePath := path.Clean(activeUser.PathJoin(driveId, paths[idx]))
|
||||
fileList, err1 := matchPathByShellPattern(driveId, absolutePath)
|
||||
if err1 != nil {
|
||||
fmt.Println("文件不存在: " + absolutePath)
|
||||
continue
|
||||
}
|
||||
if fileList == nil || len(fileList) == 0 {
|
||||
// 文件不存在
|
||||
fmt.Println("文件不存在: " + absolutePath)
|
||||
continue
|
||||
}
|
||||
// 匹配的文件
|
||||
allFileList = append(allFileList, fileList...)
|
||||
}
|
||||
|
||||
fidList := []string{}
|
||||
for _, f := range allFileList {
|
||||
fidList = append(fidList, f.FileId)
|
||||
}
|
||||
|
||||
if len(fidList) == 0 {
|
||||
fmt.Printf("没有指定有效的文件\n")
|
||||
return
|
||||
}
|
||||
|
||||
if modeFlag == "3" {
|
||||
// 快传
|
||||
r, err1 := panClient.WebapiPanClient().FastShareLinkCreate(aliyunpan.FastShareCreateParam{
|
||||
DriveId: driveId,
|
||||
FileIdList: fidList,
|
||||
})
|
||||
if err1 != nil || r == nil {
|
||||
if err1.Code == apierror.ApiCodeFileShareNotAllowed {
|
||||
fmt.Printf("创建快传链接失败: 该文件类型不允许分享\n")
|
||||
} else {
|
||||
fmt.Printf("创建快传链接失败: %s\n", err1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("创建快传链接成功\n")
|
||||
fmt.Printf("链接:%s\n", r.ShareUrl)
|
||||
} else {
|
||||
// 分享
|
||||
r, err1 := panClient.WebapiPanClient().ShareLinkCreate(aliyunpan.ShareCreateParam{
|
||||
DriveId: driveId,
|
||||
SharePwd: sharePwd,
|
||||
Expiration: expiredTime,
|
||||
FileIdList: fidList,
|
||||
})
|
||||
if err1 != nil || r == nil {
|
||||
if err1.Code == apierror.ApiCodeFileShareNotAllowed {
|
||||
fmt.Printf("创建分享链接失败: 该文件类型不允许分享\n")
|
||||
} else {
|
||||
fmt.Printf("创建分享链接失败: %s\n", err1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("创建分享链接成功\n")
|
||||
if len(sharePwd) > 0 {
|
||||
fmt.Printf("链接:%s 提取码:%s\n", r.ShareUrl, r.SharePwd)
|
||||
} else {
|
||||
fmt.Printf("链接:%s\n", r.ShareUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// RunShareList 执行列出分享列表
|
||||
func RunShareList() {
|
||||
activeUser := GetActiveUser()
|
||||
records, err := activeUser.PanClient().WebapiPanClient().ShareLinkList(activeUser.UserId)
|
||||
if err != nil {
|
||||
fmt.Printf("获取分享列表失败: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
tb := cmdtable.NewTable(os.Stdout)
|
||||
tb.SetHeader([]string{"#", "ShARE_ID", "分享链接", "提取码", "文件名", "过期时间", "状态"})
|
||||
now := time.Now()
|
||||
for k, record := range records {
|
||||
et := "永久有效"
|
||||
if len(record.Expiration) > 0 {
|
||||
et = record.Expiration
|
||||
}
|
||||
status := "有效"
|
||||
if record.Status == "enabled" {
|
||||
if record.FirstFile == nil {
|
||||
status = "已删除"
|
||||
} else {
|
||||
cz := time.FixedZone("CST", 8*3600)
|
||||
if len(record.Expiration) > 0 {
|
||||
expiredTime, _ := time.ParseInLocation("2006-01-02 15:04:05", record.Expiration, cz)
|
||||
if expiredTime.Unix() < now.Unix() {
|
||||
status = "已过期"
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if record.Status == "forbidden" {
|
||||
status = "违规"
|
||||
}
|
||||
|
||||
tb.Append([]string{strconv.Itoa(k + 1), record.ShareId, record.ShareUrl, record.SharePwd,
|
||||
record.ShareName,
|
||||
//record.FileIdList[0],
|
||||
et,
|
||||
status})
|
||||
}
|
||||
tb.Render()
|
||||
}
|
||||
|
||||
// RunShareCancel 执行取消分享
|
||||
func RunShareCancel(shareIdList []string) {
|
||||
if len(shareIdList) == 0 {
|
||||
fmt.Printf("取消分享操作失败, 没有任何 shareid\n")
|
||||
return
|
||||
}
|
||||
|
||||
activeUser := GetActiveUser()
|
||||
r, err := activeUser.PanClient().WebapiPanClient().ShareLinkCancel(shareIdList)
|
||||
if err != nil {
|
||||
fmt.Printf("取消分享操作失败: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
if r != nil && len(r) > 0 {
|
||||
fmt.Printf("取消分享操作成功\n")
|
||||
} else {
|
||||
fmt.Printf("取消分享操作失败\n")
|
||||
}
|
||||
}
|
||||
|
||||
func RunShareExport(option, saveFilePath string) {
|
||||
activeUser := GetActiveUser()
|
||||
records, err := activeUser.PanClient().WebapiPanClient().ShareLinkList(activeUser.UserId)
|
||||
if err != nil {
|
||||
fmt.Printf("获取分享列表失败: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
columns := [][]string{{"序号", "分享ID", "分享链接", "提取码", "文件名", "过期时间", "状态"}}
|
||||
now := time.Now()
|
||||
idx := 1
|
||||
for _, record := range records {
|
||||
et := "永久有效"
|
||||
if len(record.Expiration) > 0 {
|
||||
et = record.Expiration
|
||||
}
|
||||
status := "有效"
|
||||
if record.FirstFile == nil {
|
||||
status = "已删除"
|
||||
if option == "1" {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
cz := time.FixedZone("CST", 8*3600)
|
||||
if len(record.Expiration) > 0 {
|
||||
expiredTime, _ := time.ParseInLocation("2006-01-02 15:04:05", record.Expiration, cz)
|
||||
if expiredTime.Unix() < now.Unix() {
|
||||
status = "已过期"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if option == "1" {
|
||||
if status == "已过期" || status == "已删除" {
|
||||
continue
|
||||
}
|
||||
}
|
||||
line := []string{strconv.Itoa(idx), record.ShareId, record.ShareUrl, record.SharePwd, record.ShareName, et, status}
|
||||
idx += 1
|
||||
columns = append(columns, line)
|
||||
}
|
||||
|
||||
// save to file
|
||||
if ExportCsv(saveFilePath, columns) {
|
||||
fmt.Println("分享导出成功:", saveFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
func ExportCsv(savePath string, data [][]string) bool {
|
||||
folder := filepath.Dir(savePath)
|
||||
if _, err := os.Stat(folder); err != nil {
|
||||
if !os.IsExist(err) {
|
||||
os.MkdirAll(folder, os.ModePerm)
|
||||
}
|
||||
}
|
||||
fp, err := os.Create(savePath) // 创建文件句柄
|
||||
if err != nil {
|
||||
fmt.Println("创建文件["+savePath+"]失败,%v", err)
|
||||
return false
|
||||
}
|
||||
defer fp.Close()
|
||||
fp.WriteString("\xEF\xBB\xBF") // 写入UTF-8 BOM
|
||||
w := csv.NewWriter(fp) //创建一个新的写入文件流
|
||||
w.WriteAll(data)
|
||||
w.Flush()
|
||||
return true
|
||||
}
|
||||
@@ -203,8 +203,6 @@ driveName - 网盘名称,backup(备份盘),resource(资源盘)
|
||||
}
|
||||
if policy == string(syncdrive.SyncPolicyExclusive) {
|
||||
task.Policy = syncdrive.SyncPolicyExclusive
|
||||
} else if policy == string(syncdrive.SyncPolicyIncrement) {
|
||||
task.Policy = syncdrive.SyncPolicyIncrement
|
||||
} else {
|
||||
task.Policy = syncdrive.SyncPolicyIncrement
|
||||
}
|
||||
@@ -232,8 +230,12 @@ driveName - 网盘名称,backup(备份盘),resource(资源盘)
|
||||
} else {
|
||||
cycleMode = syncdrive.CycleInfiniteLoop
|
||||
}
|
||||
RunSync(task, cycleMode, dp, up, downloadBlockSize, uploadBlockSize, syncOpt, c.Int("ldt"))
|
||||
|
||||
scanIntervalTime := int64(c.Int("sit") * 60)
|
||||
if scanIntervalTime == 0 {
|
||||
// 默认1分钟
|
||||
scanIntervalTime = 60
|
||||
}
|
||||
RunSync(task, cycleMode, dp, up, downloadBlockSize, uploadBlockSize, syncOpt, c.Int("ldt"), scanIntervalTime)
|
||||
return nil
|
||||
},
|
||||
Flags: []cli.Flag{
|
||||
@@ -297,9 +299,14 @@ driveName - 网盘名称,backup(备份盘),resource(资源盘)
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "ldt",
|
||||
Usage: "local delay time,本地文件修改检测延迟间隔,单位秒。如果本地文件会被频繁修改,例如录制视频文件,配置好该时间可以避免上传未录制好的文件",
|
||||
Usage: "local delay time,本地文件修改检测延迟间隔,单位秒。如果本地文件会被频繁修改,例如录制视频文件,配置好该时间可以避免上传未录制好的文件。",
|
||||
Value: 3,
|
||||
},
|
||||
cli.IntFlag{
|
||||
Name: "sit",
|
||||
Usage: "scan interval time,扫描文件间隔时间,单位:分钟。",
|
||||
Value: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -307,7 +314,7 @@ driveName - 网盘名称,backup(备份盘),resource(资源盘)
|
||||
}
|
||||
|
||||
func RunSync(defaultTask *syncdrive.SyncTask, cycleMode syncdrive.CycleMode, fileDownloadParallel, fileUploadParallel int, downloadBlockSize, uploadBlockSize int64,
|
||||
flag syncdrive.SyncPriorityOption, localDelayTime int) {
|
||||
flag syncdrive.SyncPriorityOption, localDelayTime int, scanTimeInterval int64) {
|
||||
maxDownloadRate := config.Config.MaxDownloadRate
|
||||
maxUploadRate := config.Config.MaxUploadRate
|
||||
activeUser := GetActiveUser()
|
||||
@@ -369,7 +376,7 @@ func RunSync(defaultTask *syncdrive.SyncTask, cycleMode syncdrive.CycleMode, fil
|
||||
fmt.Printf("备份配置文件:%s\n下载并发:%d\n上传并发:%d\n下载分片大小:%s\n上传分片大小:%s\n",
|
||||
syncConfigFile, fileDownloadParallel, fileUploadParallel, converter.ConvertFileSize(downloadBlockSize, 2),
|
||||
converter.ConvertFileSize(uploadBlockSize, 2))
|
||||
if _, e := syncMgr.Start(tasks, cycleMode); e != nil {
|
||||
if _, e := syncMgr.Start(tasks, cycleMode, scanTimeInterval); e != nil {
|
||||
fmt.Println("启动任务失败:", e)
|
||||
return
|
||||
}
|
||||
@@ -391,6 +398,7 @@ func RunSync(defaultTask *syncdrive.SyncTask, cycleMode syncdrive.CycleMode, fil
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
syncMgr.DoTaskSyncCompletelyPluginCallback()
|
||||
}
|
||||
} else {
|
||||
if cycleMode == syncdrive.CycleInfiniteLoop {
|
||||
@@ -416,6 +424,7 @@ func RunSync(defaultTask *syncdrive.SyncTask, cycleMode syncdrive.CycleMode, fil
|
||||
}
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
syncMgr.DoTaskSyncCompletelyPluginCallback()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+78
-52
@@ -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"),
|
||||
})
|
||||
|
||||
// 释放文件锁
|
||||
@@ -222,21 +230,6 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
|
||||
activeUser.PanClient().OpenapiPanClient().ClearCache()
|
||||
defer activeUser.PanClient().OpenapiPanClient().DisableCache()
|
||||
|
||||
//// pan token expired checker
|
||||
//continueFlag := int32(0)
|
||||
//atomic.StoreInt32(&continueFlag, 0)
|
||||
//defer func() {
|
||||
// atomic.StoreInt32(&continueFlag, 1)
|
||||
//}()
|
||||
//go func(flag *int32) {
|
||||
// for atomic.LoadInt32(flag) == 0 {
|
||||
// time.Sleep(time.Duration(1) * time.Minute)
|
||||
// if RefreshWebTokenInNeed(activeUser, config.Config.DeviceName) {
|
||||
// logger.Verboseln("update access token for upload task")
|
||||
// }
|
||||
// }
|
||||
//}(&continueFlag)
|
||||
|
||||
if opt == nil {
|
||||
opt = &UploadOptions{}
|
||||
}
|
||||
@@ -264,7 +257,8 @@ func RunUpload(localPaths []string, savePath string, opt *UploadOptions) {
|
||||
activeUser.PanClient().OpenapiPanClient().SetTimeout(time.Duration(opt.MaxTimeoutSec) * time.Second)
|
||||
}
|
||||
|
||||
fmt.Printf("\n[0] 当前文件上传最大并发量为: %d, 上传分片大小为: %s\n", opt.AllParallel, converter.ConvertFileSize(opt.BlockSize, 2))
|
||||
targetDriveName := config.Config.ActiveUser().DriveList.GetDriveNameById(opt.DriveId)
|
||||
fmt.Printf("\n[0] 当前文件上传最大并发量为: %d, 上传分片大小为: %s, 目标网盘: %s\n", opt.AllParallel, converter.ConvertFileSize(opt.BlockSize, 2), targetDriveName)
|
||||
|
||||
savePath = activeUser.PathJoin(opt.DriveId, savePath)
|
||||
_, err1 := activeUser.PanClient().OpenapiPanClient().FileInfoByPath(opt.DriveId, savePath)
|
||||
@@ -304,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()
|
||||
|
||||
@@ -318,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
|
||||
}
|
||||
|
||||
@@ -340,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
|
||||
}
|
||||
|
||||
@@ -369,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -398,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()
|
||||
@@ -425,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代表不限制"},
|
||||
|
||||
@@ -71,6 +71,15 @@ func (d DriveInfoList) GetDriveIdByName(name string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (d DriveInfoList) GetDriveNameById(driveId string) string {
|
||||
for _, drive := range d {
|
||||
if drive.DriveId == driveId {
|
||||
return drive.DriveName
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// PanClientToken 授权Token
|
||||
type PanClientToken struct {
|
||||
// AccessToken AccessToken
|
||||
@@ -143,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 {
|
||||
@@ -160,7 +176,11 @@ doOpenLoginAct:
|
||||
AccessToken: wt.AccessToken,
|
||||
Expired: wt.Expired,
|
||||
}
|
||||
|
||||
// TODO: 检查token有效期
|
||||
}
|
||||
|
||||
// 回到token检测流程,必须确保token是有效
|
||||
time.Sleep(time.Duration(1) * time.Second)
|
||||
goto doOpenLoginAct
|
||||
}
|
||||
@@ -198,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{
|
||||
@@ -237,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 {
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan/apierror"
|
||||
"github.com/tickstep/aliyunpan/cmder/cmdutil"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/global"
|
||||
"github.com/tickstep/aliyunpan/internal/waitgroup"
|
||||
"github.com/tickstep/aliyunpan/library/requester/transfer"
|
||||
"github.com/tickstep/library-go/cachepool"
|
||||
@@ -54,6 +55,7 @@ type (
|
||||
monitorCancelFunc context.CancelFunc
|
||||
globalSpeedsStat *speeds.Speeds // 全局速度统计
|
||||
|
||||
filePanSource global.FileSourceType // 要下载的网盘文件来源
|
||||
fileInfo *aliyunpan.FileEntity // 下载的文件信息
|
||||
driveId string
|
||||
loadBalancerCompareFunc LoadBalancerCompareFunc // 负载均衡检测函数
|
||||
@@ -97,11 +99,11 @@ func NewDownloader(writer io.WriterAt, config *Config, p *config.PanClient, sp [
|
||||
return
|
||||
}
|
||||
|
||||
// SetClient 设置http客户端
|
||||
func (der *Downloader) SetFileInfo(f *aliyunpan.FileEntity) {
|
||||
// SetFileInfo 设置文件信息
|
||||
func (der *Downloader) SetFileInfo(source global.FileSourceType, f *aliyunpan.FileEntity) {
|
||||
der.filePanSource = source
|
||||
der.fileInfo = f
|
||||
}
|
||||
|
||||
func (der *Downloader) SetDriveId(driveId string) {
|
||||
der.driveId = driveId
|
||||
}
|
||||
@@ -389,27 +391,7 @@ func (der *Downloader) Execute() error {
|
||||
writeMu = &sync.Mutex{}
|
||||
)
|
||||
|
||||
// 获取下载链接
|
||||
//var apierr *apierror.ApiError
|
||||
//durl, apierr := der.panClient.OpenapiPanClient().GetFileDownloadUrl(&aliyunpan.GetFileDownloadUrlParam{
|
||||
// DriveId: der.driveId,
|
||||
// FileId: der.fileInfo.FileId,
|
||||
//})
|
||||
//time.Sleep(time.Duration(200) * time.Millisecond)
|
||||
//if apierr != nil {
|
||||
// logger.Verbosef("ERROR: get download url error: %s\n", der.fileInfo.FileId)
|
||||
// cmdutil.Trigger(der.onCancelEvent)
|
||||
// return apierr
|
||||
//}
|
||||
//if durl == nil || durl.Url == "" || strings.HasPrefix(durl.Url, aliyunpan.IllegalDownloadUrlPrefix) {
|
||||
// logger.Verbosef("无法获取有效的下载链接: %+v\n", durl)
|
||||
// cmdutil.Trigger(der.onCancelEvent)
|
||||
// der.removeInstanceState() // 移除断点续传文件
|
||||
// cmdutil.Trigger(der.onFailedEvent)
|
||||
// return ErrFileDownloadForbidden
|
||||
//}
|
||||
|
||||
// 获取各个网盘客户端的下载链接
|
||||
// 获取各个账号网盘客户端的下载链接
|
||||
panClientFileUrl, err := der.getFileAllClientDownloadUrl()
|
||||
if err != nil || panClientFileUrl == nil {
|
||||
logger.Verbosef("ERROR: get download url error: %s\n", der.fileInfo.FileId)
|
||||
@@ -476,8 +458,19 @@ func (der *Downloader) Execute() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 获取对应网盘的下载链接
|
||||
// 获取对应网盘文件的下载链接
|
||||
func (der *Downloader) getFileAllClientDownloadUrl() ([]*panClientDownloadUrlEntity, error) {
|
||||
if der.filePanSource == global.AlbumSource {
|
||||
// 相册源只支持主账号下载
|
||||
return der.getAlbumSourceDownloadUrl()
|
||||
} else {
|
||||
// 文件源支持多账号分流下载
|
||||
return der.getFileSourceDownloadUrl()
|
||||
}
|
||||
}
|
||||
|
||||
// getFileSourceDownloadUrl 获取文件源的文件下载链接
|
||||
func (der *Downloader) getFileSourceDownloadUrl() ([]*panClientDownloadUrlEntity, error) {
|
||||
result := []*panClientDownloadUrlEntity{}
|
||||
|
||||
// 主账号(必须存在)
|
||||
@@ -523,7 +516,7 @@ func (der *Downloader) getFileAllClientDownloadUrl() ([]*panClientDownloadUrlEnt
|
||||
// 网盘文件路径
|
||||
mainFileFullPath := der.fileInfo.Path
|
||||
|
||||
// 铺助账号的下载链接
|
||||
// 铺助账号的下载链接,只支持文件源
|
||||
if der.subPanClientList != nil {
|
||||
for _, spc := range der.subPanClientList {
|
||||
driveId := ""
|
||||
@@ -570,6 +563,60 @@ func (der *Downloader) getFileAllClientDownloadUrl() ([]*panClientDownloadUrlEnt
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// getAlbumSourceDownloadUrl 获取相册源的文件下载链接
|
||||
func (der *Downloader) getAlbumSourceDownloadUrl() ([]*panClientDownloadUrlEntity, error) {
|
||||
result := []*panClientDownloadUrlEntity{}
|
||||
|
||||
// 相册源只能使用主账号
|
||||
// 获取下载链接
|
||||
var apierr *apierror.ApiError
|
||||
durl, apierr := der.panClient.OpenapiPanClient().ShareAlbumGetFileDownloadUrl(&aliyunpan.ShareAlbumGetFileUrlParam{
|
||||
AlbumId: der.fileInfo.AlbumId,
|
||||
DriveId: der.fileInfo.DriveId,
|
||||
FileId: der.fileInfo.FileId,
|
||||
})
|
||||
time.Sleep(time.Duration(200) * time.Millisecond)
|
||||
if apierr != nil {
|
||||
logger.Verbosef("ERROR: get album file download url error: %s\n", der.fileInfo.FileId)
|
||||
cmdutil.Trigger(der.onCancelEvent)
|
||||
return nil, apierr
|
||||
}
|
||||
realUrl := ""
|
||||
if durl.StreamsUrl != nil { // 实况图片
|
||||
if strings.ToLower(der.fileInfo.FileExtension) == "mov" {
|
||||
// 视频文件
|
||||
if durl.StreamsUrl.Mov != "" {
|
||||
realUrl = durl.StreamsUrl.Mov
|
||||
}
|
||||
} else {
|
||||
// 照片文件
|
||||
if durl.StreamsUrl.Heic != "" {
|
||||
realUrl = durl.StreamsUrl.Heic
|
||||
} else if durl.StreamsUrl.Jpeg != "" {
|
||||
realUrl = durl.StreamsUrl.Jpeg
|
||||
}
|
||||
}
|
||||
} else if durl.Url != "" { // 普通图片
|
||||
realUrl = durl.Url
|
||||
}
|
||||
if realUrl == "" {
|
||||
logger.Verbosef("无法获取有效的相册文件下载链接: %+v\n", durl)
|
||||
cmdutil.Trigger(der.onCancelEvent)
|
||||
der.removeInstanceState() // 移除断点续传文件
|
||||
cmdutil.Trigger(der.onFailedEvent)
|
||||
return nil, ErrFileDownloadForbidden
|
||||
}
|
||||
// 返回下载链接
|
||||
result = append(result, &panClientDownloadUrlEntity{
|
||||
PanClient: der.panClient,
|
||||
FileInfo: der.fileInfo,
|
||||
DriveId: der.driveId,
|
||||
FileId: der.fileInfo.FileId,
|
||||
FileUrl: realUrl,
|
||||
})
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// downloadStatusEvent 执行状态处理事件
|
||||
func (der *Downloader) downloadStatusEvent() {
|
||||
if der.onDownloadStatusEvent == nil {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
@@ -30,6 +30,7 @@ type (
|
||||
io.Seeker
|
||||
Range() transfer.Range
|
||||
Left() int64
|
||||
ResetReader(readerAt io.ReaderAt)
|
||||
}
|
||||
|
||||
fileBlock struct {
|
||||
@@ -147,3 +148,8 @@ func (fb *fileBlock) Range() transfer.Range {
|
||||
func (fb *fileBlock) Readed() int64 {
|
||||
return fb.readed
|
||||
}
|
||||
|
||||
func (fb *fileBlock) ResetReader(readerAt io.ReaderAt) {
|
||||
fb.readerAt = readerAt
|
||||
fb.readed = 0
|
||||
}
|
||||
|
||||
@@ -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,10 +16,13 @@ package uploader
|
||||
import "fmt"
|
||||
|
||||
var (
|
||||
UploadUrlExpired = fmt.Errorf("UrlExpired")
|
||||
UploadPartNotSeq = fmt.Errorf("PartNotSequential")
|
||||
UploadTerminate = fmt.Errorf("UploadErrorTerminate")
|
||||
UploadPartAlreadyExist = fmt.Errorf("PartAlreadyExist")
|
||||
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 (
|
||||
@@ -33,5 +36,8 @@ type (
|
||||
)
|
||||
|
||||
func (me *MultiError) Error() string {
|
||||
return me.Err.Error()
|
||||
if me.Err != nil {
|
||||
return me.Err.Error()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@@ -20,9 +20,9 @@ import (
|
||||
type (
|
||||
// BlockState 文件区块信息
|
||||
BlockState struct {
|
||||
ID int `json:"id"`
|
||||
Range transfer.Range `json:"range"`
|
||||
UploadDone bool `json:"upload_done"`
|
||||
ID int `json:"id"`
|
||||
Range transfer.Range `json:"range"`
|
||||
UploadDone bool `json:"upload_done"`
|
||||
}
|
||||
|
||||
// InstanceState 上传断点续传信息
|
||||
@@ -39,18 +39,14 @@ func (muer *MultiUploader) getWorkerListByInstanceState(is *InstanceState) worke
|
||||
id: blockState.ID,
|
||||
partOffset: blockState.Range.Begin,
|
||||
splitUnit: NewBufioSplitUnit(muer.file, blockState.Range, muer.speedsStat, muer.rateLimit, muer.globalSpeedsStat),
|
||||
uploadDone: false,
|
||||
uploadDone: false,
|
||||
})
|
||||
} else {
|
||||
// 已经完成的, 也要加入 (可继续优化)
|
||||
// 已经完成的, 也要加入
|
||||
workers = append(workers, &worker{
|
||||
id: blockState.ID,
|
||||
partOffset: blockState.Range.Begin,
|
||||
splitUnit: &fileBlock{
|
||||
readRange: blockState.Range,
|
||||
readed: blockState.Range.End - blockState.Range.Begin,
|
||||
readerAt: muer.file,
|
||||
},
|
||||
splitUnit: NewBufioSplitUnit(muer.file, blockState.Range, muer.speedsStat, muer.rateLimit, muer.globalSpeedsStat),
|
||||
uploadDone: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,6 +16,7 @@ package uploader
|
||||
import (
|
||||
"context"
|
||||
"github.com/tickstep/aliyunpan-api/aliyunpan"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/utils"
|
||||
"github.com/tickstep/library-go/converter"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
@@ -61,6 +62,7 @@ type (
|
||||
|
||||
// 网盘上传参数
|
||||
UploadOpEntity *aliyunpan.CreateFileUploadResult `json:"uploadOpEntity"`
|
||||
panClient *config.PanClient
|
||||
}
|
||||
|
||||
// MultiUploaderConfig 多线程上传配置
|
||||
@@ -72,12 +74,13 @@ type (
|
||||
)
|
||||
|
||||
// NewMultiUploader 初始化上传
|
||||
func NewMultiUploader(multiUpload MultiUpload, file rio.ReaderAtLen64, config *MultiUploaderConfig, uploadOpEntity *aliyunpan.CreateFileUploadResult, globalSpeedsStat *speeds.Speeds) *MultiUploader {
|
||||
func NewMultiUploader(multiUpload MultiUpload, file rio.ReaderAtLen64, config *MultiUploaderConfig, uploadOpEntity *aliyunpan.CreateFileUploadResult, panClient *config.PanClient, globalSpeedsStat *speeds.Speeds) *MultiUploader {
|
||||
return &MultiUploader{
|
||||
multiUpload: multiUpload,
|
||||
file: file,
|
||||
config: config,
|
||||
UploadOpEntity: uploadOpEntity,
|
||||
panClient: panClient,
|
||||
globalSpeedsStat: globalSpeedsStat,
|
||||
}
|
||||
}
|
||||
@@ -137,13 +140,13 @@ func (muer *MultiUploader) Execute() error {
|
||||
// 分配任务
|
||||
if muer.instanceState != nil {
|
||||
muer.workers = muer.getWorkerListByInstanceState(muer.instanceState)
|
||||
logger.Verboseln("upload task CREATED from instance state\n")
|
||||
logger.Verbosef("upload task CREATED from instance state\n")
|
||||
} else {
|
||||
muer.workers = muer.getWorkerListByInstanceState(&InstanceState{
|
||||
BlockList: SplitBlock(muer.file.Len(), muer.config.BlockSize),
|
||||
})
|
||||
|
||||
logger.Verboseln("upload task CREATED: block size: %d, num: %d\n", muer.config.BlockSize, len(muer.workers))
|
||||
logger.Verbosef("upload task CREATED: block size: %d, worker num: %d\n", muer.config.BlockSize, len(muer.workers))
|
||||
}
|
||||
|
||||
// 开始上传
|
||||
@@ -195,32 +198,32 @@ func (muer *MultiUploader) Cancel() {
|
||||
close(muer.canceled)
|
||||
}
|
||||
|
||||
//OnExecute 设置开始上传事件
|
||||
// OnExecute 设置开始上传事件
|
||||
func (muer *MultiUploader) OnExecute(onExecuteEvent requester.Event) {
|
||||
muer.onExecuteEvent = onExecuteEvent
|
||||
}
|
||||
|
||||
//OnSuccess 设置成功上传事件
|
||||
// OnSuccess 设置成功上传事件
|
||||
func (muer *MultiUploader) OnSuccess(onSuccessEvent requester.Event) {
|
||||
muer.onSuccessEvent = onSuccessEvent
|
||||
}
|
||||
|
||||
//OnFinish 设置结束上传事件
|
||||
// OnFinish 设置结束上传事件
|
||||
func (muer *MultiUploader) OnFinish(onFinishEvent requester.Event) {
|
||||
muer.onFinishEvent = onFinishEvent
|
||||
}
|
||||
|
||||
//OnCancel 设置取消上传事件
|
||||
// OnCancel 设置取消上传事件
|
||||
func (muer *MultiUploader) OnCancel(onCancelEvent requester.Event) {
|
||||
muer.onCancelEvent = onCancelEvent
|
||||
}
|
||||
|
||||
//OnError 设置上传发生错误事件
|
||||
// OnError 设置上传发生错误事件
|
||||
func (muer *MultiUploader) OnError(onErrorEvent requester.EventOnError) {
|
||||
muer.onErrorEvent = onErrorEvent
|
||||
}
|
||||
|
||||
//OnUploadStatusEvent 设置上传状态事件
|
||||
// OnUploadStatusEvent 设置上传状态事件
|
||||
func (muer *MultiUploader) OnUploadStatusEvent(f UploadStatusFunc) {
|
||||
muer.onUploadStatusEvent = f
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@@ -15,17 +15,18 @@ package uploader
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/oleiade/lane"
|
||||
"github.com/tickstep/aliyunpan/internal/waitgroup"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"github.com/tickstep/library-go/requester"
|
||||
"os"
|
||||
"io"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type (
|
||||
worker struct {
|
||||
id int
|
||||
id int // ID从0开始计数
|
||||
partOffset int64
|
||||
splitUnit SplitUnit
|
||||
uploadDone bool
|
||||
@@ -34,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
|
||||
}
|
||||
@@ -89,7 +97,7 @@ func (muer *MultiUploader) upload() (uperr error) {
|
||||
)
|
||||
go func() {
|
||||
if !wer.uploadDone {
|
||||
logger.Verboseln("begin to upload part: " + strconv.Itoa(wer.id))
|
||||
logger.Verboseln("begin to upload part num: " + strconv.Itoa(wer.id+1))
|
||||
uploadDone, terr = muer.multiUpload.UploadFile(ctx, int(wer.id), wer.partOffset, wer.splitUnit.Range().End, wer.splitUnit, uploadClient)
|
||||
} else {
|
||||
uploadDone = true
|
||||
@@ -102,11 +110,11 @@ func (muer *MultiUploader) upload() (uperr error) {
|
||||
return
|
||||
case <-doneChan:
|
||||
// continue
|
||||
logger.Verboseln("multiUpload worker upload file done")
|
||||
logger.Verboseln("multiUpload worker upload file http action done")
|
||||
}
|
||||
cancel()
|
||||
if terr != nil {
|
||||
logger.Verboseln("upload file part err: %+v", terr)
|
||||
logger.Verbosef("upload file part err: %+v\n", terr)
|
||||
if me, ok := terr.(*MultiError); ok {
|
||||
if me.Terminated { // 终止
|
||||
muer.closeCanceledOnce.Do(func() { // 只关闭一次
|
||||
@@ -115,18 +123,21 @@ func (muer *MultiUploader) upload() (uperr error) {
|
||||
uperr = me.Err
|
||||
return
|
||||
} else if me.NeedStartOver {
|
||||
logger.Verboseln("upload start over: %d\n", wer.id)
|
||||
logger.Verbosef("upload start over: %d\n", wer.id)
|
||||
// 从头开始上传
|
||||
muer.closeCanceledOnce.Do(func() { // 只关闭一次
|
||||
close(muer.canceled)
|
||||
})
|
||||
uperr = me.Err
|
||||
return
|
||||
} else {
|
||||
uperr = me.Err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
logger.Verboseln("upload err: %s, id: %d\n", terr, wer.id)
|
||||
wer.splitUnit.Seek(0, os.SEEK_SET)
|
||||
logger.Verbosef("upload worker err: %s, id: %d\n", terr, wer.id)
|
||||
wer.splitUnit.Seek(0, io.SeekStart)
|
||||
uploadDeque.Prepend(wer) // 放回上传队列首位
|
||||
return
|
||||
}
|
||||
@@ -139,9 +150,11 @@ func (muer *MultiUploader) upload() (uperr error) {
|
||||
}()
|
||||
wg.Wait()
|
||||
if uperr != nil {
|
||||
if uperr == UploadPartNotSeq {
|
||||
// 分片出现乱序,需要重新上传,取消本次所有剩余的分片的上传
|
||||
break
|
||||
if errors.Is(uperr, UploadPartNotSeq) ||
|
||||
errors.Is(uperr, UploadNoSuchUpload) ||
|
||||
errors.Is(uperr, UploadLocalFileAlreadyClosedError) {
|
||||
// 清空数据,停止上传
|
||||
uploadDeque = lane.NewDeque() // 清空待上传列表
|
||||
}
|
||||
}
|
||||
// 没有任务了
|
||||
@@ -153,6 +166,13 @@ func (muer *MultiUploader) upload() (uperr error) {
|
||||
// 释放链路
|
||||
uploadClient.CloseIdleConnections()
|
||||
|
||||
// 返回错误,通知上层客户端进行处理
|
||||
if errors.Is(uperr, UploadPartNotSeq) ||
|
||||
errors.Is(uperr, UploadNoSuchUpload) ||
|
||||
errors.Is(uperr, UploadLocalFileAlreadyClosedError) {
|
||||
return uperr
|
||||
}
|
||||
|
||||
select {
|
||||
case <-muer.canceled:
|
||||
if uperr != nil {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
package uploader
|
||||
|
||||
import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -24,6 +25,7 @@ type (
|
||||
Uploaded() int64 // 已上传数据
|
||||
SpeedsPerSecond() int64 // 每秒的上传速度
|
||||
TimeElapsed() time.Duration // 上传时间
|
||||
TimeLeft() time.Duration // 预计剩余时间, 负数代表未知
|
||||
}
|
||||
|
||||
// UploadStatus 上传状态
|
||||
@@ -67,6 +69,18 @@ func (us *UploadStatus) TimeElapsed() time.Duration {
|
||||
return us.timeElapsed
|
||||
}
|
||||
|
||||
// TimeLeft 返回预计剩余时间, 负数代表未知
|
||||
func (us *UploadStatus) TimeLeft() time.Duration {
|
||||
var left time.Duration
|
||||
speeds := atomic.LoadInt64(&us.speedsPerSecond)
|
||||
if speeds <= 0 {
|
||||
left = -1
|
||||
} else {
|
||||
left = time.Duration((us.totalSize-us.uploaded)/(speeds)) * time.Second
|
||||
}
|
||||
return left
|
||||
}
|
||||
|
||||
// GetStatusChan 获取上传状态
|
||||
func (u *Uploader) GetStatusChan() <-chan Status {
|
||||
c := make(chan Status)
|
||||
|
||||
@@ -15,36 +15,40 @@ 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"
|
||||
"github.com/tickstep/aliyunpan/internal/config"
|
||||
"github.com/tickstep/aliyunpan/internal/file/downloader"
|
||||
"github.com/tickstep/aliyunpan/internal/functions"
|
||||
"github.com/tickstep/aliyunpan/internal/global"
|
||||
"github.com/tickstep/aliyunpan/internal/localfile"
|
||||
"github.com/tickstep/aliyunpan/internal/log"
|
||||
"github.com/tickstep/aliyunpan/internal/plugins"
|
||||
"github.com/tickstep/aliyunpan/internal/taskframework"
|
||||
"github.com/tickstep/aliyunpan/internal/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 (
|
||||
FileSourceType string
|
||||
|
||||
// DownloadTaskUnit 下载的任务单元
|
||||
DownloadTaskUnit struct {
|
||||
taskInfo *taskframework.TaskInfo // 任务信息
|
||||
// DownloadActionId 下载动作ID,唯一标识一次下载动作的ID。这个ID每次下载任务启动的时候会自动生成,同一次下载任务下载的文件,这个ID都是同一个值。
|
||||
DownloadActionId string
|
||||
taskInfo *taskframework.TaskInfo // 任务信息
|
||||
|
||||
Cfg *downloader.Config
|
||||
PanClient *config.PanClient
|
||||
@@ -56,21 +60,23 @@ type (
|
||||
|
||||
// 可选项
|
||||
VerbosePrinter *logger.CmdVerbose
|
||||
PrintFormat string
|
||||
IsPrintStatus bool // 是否输出各个下载线程的详细信息
|
||||
IsExecutedPermission bool // 下载成功后是否加上执行权限
|
||||
IsOverwrite bool // 是否覆盖已存在的文件
|
||||
NoCheck bool // 不校验文件
|
||||
|
||||
FilePanPath string // 要下载的网盘文件路径
|
||||
SavePath string // 文件保存在本地的路径
|
||||
OriginSaveRootPath string // 文件保存在本地的根目录路径
|
||||
DriveId string
|
||||
FilePanSource global.FileSourceType // 要下载的网盘文件来源
|
||||
FilePanPath string // 要下载的网盘文件路径
|
||||
SavePath string // 文件保存在本地的路径
|
||||
OriginSaveRootPath string // 文件保存在本地的根目录路径
|
||||
DriveId string // 网盘ID
|
||||
|
||||
fileInfo *aliyunpan.FileEntity // 文件或目录详情
|
||||
|
||||
// 下载文件记录器
|
||||
FileRecorder *log.FileRecorder
|
||||
|
||||
UI *ui.DashboardPanel // 下载统计面板UI
|
||||
}
|
||||
)
|
||||
|
||||
@@ -89,15 +95,14 @@ const (
|
||||
StrDownloadChecksumFailed = "检测文件有效性失败"
|
||||
// DefaultDownloadMaxRetry 默认下载失败最大重试次数
|
||||
DefaultDownloadMaxRetry = 3
|
||||
|
||||
// BackupFileSource 备份盘文件
|
||||
BackupFileSource FileSourceType = "backup"
|
||||
// ResourceFileSource 资源库文件
|
||||
ResourceFileSource FileSourceType = "resource"
|
||||
// AlbumFileSource 相册文件
|
||||
AlbumFileSource FileSourceType = "album"
|
||||
)
|
||||
|
||||
// SetFileInfo 设置文件信息
|
||||
func (dtu *DownloadTaskUnit) SetFileInfo(source global.FileSourceType, f *aliyunpan.FileEntity) {
|
||||
dtu.FilePanSource = source
|
||||
dtu.fileInfo = f
|
||||
}
|
||||
|
||||
func (dtu *DownloadTaskUnit) SetTaskInfo(info *taskframework.TaskInfo) {
|
||||
dtu.taskInfo = info
|
||||
}
|
||||
@@ -108,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 (
|
||||
@@ -172,21 +197,24 @@ func (dtu *DownloadTaskUnit) download() (err error) {
|
||||
defer file.Close()
|
||||
|
||||
der := downloader.NewDownloader(writer, dtu.Cfg, dtu.PanClient, dtu.SubPanClientList, dtu.GlobalSpeedsStat)
|
||||
der.SetFileInfo(dtu.fileInfo)
|
||||
der.SetFileInfo(dtu.FilePanSource, dtu.fileInfo)
|
||||
der.SetDriveId(dtu.DriveId)
|
||||
der.SetStatusCodeBodyCheckFunc(func(respBody io.Reader) 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 {
|
||||
@@ -234,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()
|
||||
@@ -251,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 {
|
||||
// 下载发生错误
|
||||
@@ -276,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
|
||||
}
|
||||
@@ -319,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())
|
||||
}
|
||||
|
||||
// 就在这里处理校验出错
|
||||
@@ -332,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:
|
||||
// 违规文件
|
||||
@@ -350,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
|
||||
}
|
||||
|
||||
@@ -358,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() {
|
||||
@@ -387,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) {
|
||||
@@ -403,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,
|
||||
@@ -424,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 {
|
||||
@@ -451,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"
|
||||
@@ -464,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,
|
||||
@@ -476,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
|
||||
@@ -485,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -548,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
|
||||
}
|
||||
|
||||
@@ -568,12 +614,17 @@ func (dtu *DownloadTaskUnit) Run() (result *taskframework.TaskUnitRunResult) {
|
||||
newCfg := *dtu.Cfg
|
||||
subUnit.Cfg = &newCfg
|
||||
subUnit.fileInfo = fileList[k] // 保存文件信息
|
||||
subUnit.FilePanSource = dtu.FilePanSource
|
||||
subUnit.FilePanPath = fileList[k].Path
|
||||
subUnit.SavePath = filepath.Join(dtu.OriginSaveRootPath, fileList[k].Path) // 保存位置
|
||||
|
||||
// 加入父队列,按照队列调度进行下载
|
||||
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
|
||||
@@ -116,7 +127,7 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
|
||||
}
|
||||
resp, err = uploadClient.Req(httpMethod, fullUrl, r, headers)
|
||||
if err != nil {
|
||||
logger.Verbosef("分片上传出错: 分片%d => %s\n", partseq, err)
|
||||
logger.Verbosef("分片上传出错: 分片%d => %s\n", partseq+1, err)
|
||||
}
|
||||
|
||||
if resp != nil {
|
||||
@@ -124,17 +135,25 @@ 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, 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 || "NoSuchUpload" == errResp.Code {
|
||||
if "PartNotSequential" == errResp.Code {
|
||||
respError = uploader.UploadPartNotSeq
|
||||
respErr = &uploader.MultiError{
|
||||
Err: uploader.UploadPartNotSeq,
|
||||
Terminated: false,
|
||||
NeedStartOver: true,
|
||||
NeedStartOver: false,
|
||||
}
|
||||
return resp, respError
|
||||
} else if "NoSuchUpload" == errResp.Code {
|
||||
respError = uploader.UploadNoSuchUpload
|
||||
respErr = &uploader.MultiError{
|
||||
Err: uploader.UploadNoSuchUpload,
|
||||
Terminated: true,
|
||||
NeedStartOver: false,
|
||||
}
|
||||
return resp, respError
|
||||
} else if "AccessDenied" == errResp.Code && "Request has expired." == errResp.Message {
|
||||
@@ -153,10 +172,18 @@ func (pu *PanUpload) UploadFile(ctx context.Context, partseq int, partOffset int
|
||||
return resp, respError
|
||||
}
|
||||
}
|
||||
} else {
|
||||
respError = uploader.UploadHttpError
|
||||
respErr = &uploader.MultiError{
|
||||
Err: uploader.UploadHttpError,
|
||||
Terminated: false,
|
||||
NeedStartOver: false,
|
||||
}
|
||||
return resp, respError
|
||||
}
|
||||
}
|
||||
} else {
|
||||
logger.Verbosef("分片上传出错: %d分片 => 原因未知\n", partseq)
|
||||
logger.Verbosef("分片上传出错: %d分片 => 原因未知\n", partseq+1)
|
||||
}
|
||||
|
||||
// 不可恢复的错误
|
||||
@@ -168,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
|
||||
}
|
||||
@@ -181,13 +229,13 @@ 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,
|
||||
FileId: pu.uploadOpEntity.FileId,
|
||||
UploadId: pu.uploadOpEntity.UploadId,
|
||||
PartInfoList: []aliyunpan.FileUploadPartInfoParam{{PartNumber: (partseq + 1)}}, // 阿里云盘partNum从1开始计数,partSeq从0开始
|
||||
PartInfoList: []aliyunpan.FileUploadPartInfoParam{{PartNumber: partseq + 1}}, // 阿里云盘partNum从1开始计数,partSeq从0开始
|
||||
})
|
||||
if er != nil {
|
||||
return false, &uploader.MultiError{
|
||||
@@ -197,15 +245,18 @@ 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 {
|
||||
// 上传分片乱序了,需要重新从0分片开始上传
|
||||
// 先直接返回,后续再优化
|
||||
} 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
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
@@ -15,6 +15,8 @@ package panupload
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/tickstep/library-go/logger"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -47,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
|
||||
}
|
||||
|
||||
@@ -64,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
|
||||
@@ -86,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 {
|
||||
@@ -100,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 {
|
||||
@@ -158,18 +206,20 @@ func (ud *UploadingDatabase) Search(meta *localfile.LocalFileMeta) *uploader.Ins
|
||||
if uploading.LocalFileMeta == nil {
|
||||
continue
|
||||
}
|
||||
if uploading.LocalFileMeta.EqualLengthSHA1(meta) {
|
||||
return uploading.State
|
||||
}
|
||||
if uploading.LocalFileMeta.Path.LogicPath == meta.Path.LogicPath {
|
||||
// 移除旧的信息
|
||||
// 目前只是比较了文件大小
|
||||
if meta.Length != uploading.LocalFileMeta.Length {
|
||||
// 优选通过SHA1进行匹配,如果匹配失败则通过文件路径进行匹配
|
||||
// 因为部分上传文件是直接上传的,没有计算SHA1
|
||||
if uploading.LocalFileMeta.EqualLengthSHA1(meta) ||
|
||||
uploading.LocalFileMeta.Path.LogicPath == meta.Path.LogicPath {
|
||||
// 文件大小或者修改日期不一致,代表本地文件已经更改了,保存的旧的上传文件信息已经无用
|
||||
// 移除旧的信息,客户端需要重新上传该文件
|
||||
if meta.Length != uploading.LocalFileMeta.Length ||
|
||||
meta.ModTime != uploading.LocalFileMeta.ModTime {
|
||||
logger.Verboseln("本地文件已经被修改,上传数据记录需要移除,文件需要重新从0开始上传: {}", meta.Path.LogicPath)
|
||||
ud.Delete(meta)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 覆盖数据
|
||||
// 从上传数据库补全信息并返回
|
||||
meta.SHA1 = uploading.LocalFileMeta.SHA1
|
||||
meta.ParentFolderId = uploading.LocalFileMeta.ParentFolderId
|
||||
meta.UploadOpEntity = uploading.LocalFileMeta.UploadOpEntity
|
||||
|
||||
@@ -14,10 +14,8 @@
|
||||
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"
|
||||
@@ -25,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"
|
||||
|
||||
@@ -78,6 +81,8 @@ type (
|
||||
|
||||
// 上传文件记录器
|
||||
FileRecorder *log.FileRecorder
|
||||
|
||||
UI *ui.DashboardPanel // 上传统计面板UI
|
||||
}
|
||||
)
|
||||
|
||||
@@ -96,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
|
||||
}
|
||||
@@ -126,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
|
||||
//}
|
||||
@@ -141,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
|
||||
@@ -167,7 +192,14 @@ func (utu *UploadTaskUnit) upload() (result *taskframework.TaskUnitRunResult) {
|
||||
Parallel: utu.Parallel,
|
||||
BlockSize: utu.BlockSize,
|
||||
MaxRate: config.Config.MaxUploadRate,
|
||||
}, utu.LocalFileChecksum.UploadOpEntity, utu.GlobalSpeedsStat)
|
||||
}, 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 {
|
||||
@@ -182,15 +214,35 @@ 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 {
|
||||
uploadedPercentage := fmt.Sprintf("%.2f%%", float64(status.Uploaded())/float64(status.TotalSize())*100)
|
||||
fmt.Printf("\r[%s] ↑ %s/%s(%s) %s/s(%s/s) in %s ............", utu.taskInfo.Id(),
|
||||
// 如果上传速度为0, 剩余时间未知, 则用 - 代替
|
||||
var leftStr string
|
||||
left := status.TimeLeft()
|
||||
if left < 0 {
|
||||
leftStr = "-"
|
||||
} else {
|
||||
leftStr = left.String()
|
||||
}
|
||||
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,
|
||||
converter.ConvertFileSize(status.SpeedsPerSecond(), 2),
|
||||
converter.ConvertFileSize(utu.GlobalSpeedsStat.GetSpeeds(), 2),
|
||||
status.TimeElapsed(),
|
||||
leftStr,
|
||||
)
|
||||
}
|
||||
})
|
||||
@@ -198,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) // 删除
|
||||
@@ -231,6 +283,10 @@ func (utu *UploadTaskUnit) upload() (result *taskframework.TaskUnitRunResult) {
|
||||
if er != nil {
|
||||
result.ResultMessage = StrUploadFailed
|
||||
result.NeedRetry = true
|
||||
if errors.Is(er, uploader.UploadNoSuchUpload) {
|
||||
// do not need retry
|
||||
result.NeedRetry = false
|
||||
}
|
||||
result.Err = er
|
||||
}
|
||||
return
|
||||
@@ -240,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{
|
||||
@@ -264,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) {
|
||||
@@ -295,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())
|
||||
@@ -304,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() // 关闭文件
|
||||
@@ -312,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
|
||||
@@ -323,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)))
|
||||
}()
|
||||
|
||||
// 准备文件
|
||||
@@ -359,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
|
||||
@@ -408,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
|
||||
}
|
||||
}
|
||||
@@ -431,9 +498,9 @@ StepUploadPrepareUpload:
|
||||
}
|
||||
}
|
||||
|
||||
if preHashMatch {
|
||||
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 {
|
||||
@@ -453,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"
|
||||
@@ -466,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
|
||||
@@ -479,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -504,16 +571,20 @@ StepUploadPrepareUpload:
|
||||
ContentHashName: contentHashName,
|
||||
ProofCode: proofCode,
|
||||
ProofVersion: "v1",
|
||||
LocalCreatedAt: utils.UnixTime2LocalFormatStr(utu.LocalFileChecksum.ModTime),
|
||||
LocalModifiedAt: utils.UnixTime2LocalFormatStr(utu.LocalFileChecksum.ModTime),
|
||||
}
|
||||
} else {
|
||||
// 不支持秒传,不计算SHA1,直接上传文件
|
||||
appCreateUploadFileParam = &aliyunpan.CreateFileUploadParam{
|
||||
DriveId: utu.DriveId,
|
||||
Name: filepath.Base(utu.SavePath),
|
||||
Size: utu.LocalFileChecksum.Length,
|
||||
CheckNameMode: checkNameMode,
|
||||
ParentFileId: rs.FileId,
|
||||
BlockSize: utu.BlockSize,
|
||||
DriveId: utu.DriveId,
|
||||
Name: filepath.Base(utu.SavePath),
|
||||
Size: utu.LocalFileChecksum.Length,
|
||||
CheckNameMode: checkNameMode,
|
||||
ParentFileId: rs.FileId,
|
||||
BlockSize: utu.BlockSize,
|
||||
LocalCreatedAt: utils.UnixTime2LocalFormatStr(utu.LocalFileChecksum.ModTime),
|
||||
LocalModifiedAt: utils.UnixTime2LocalFormatStr(utu.LocalFileChecksum.ModTime),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -525,6 +596,8 @@ StepUploadPrepareUpload:
|
||||
logger.Verboseln("create upload file error: " + result.ResultMessage)
|
||||
// 重试
|
||||
result.NeedRetry = true
|
||||
} else if apierr.Code == apierror.ApiCodeUploadPayloadTooLarge {
|
||||
utu.logf("[%s] %s 上传文件的大小超出限制,你可能需要开通阿里云盘三方权益包以便上传大文件\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -546,14 +619,101 @@ stepUploadUpload:
|
||||
// 正常上传流程
|
||||
uploadResult := utu.upload()
|
||||
if uploadResult != nil && uploadResult.Err != nil {
|
||||
if uploadResult.Err == uploader.UploadPartNotSeq {
|
||||
fmt.Printf("[%s] %s 文件分片上传顺序错误,开始重新上传文件\n", utu.taskInfo.Id(), time.Now().Format("2006-01-02 15:04:06"))
|
||||
// 处理上传错误
|
||||
if errors.Is(uploadResult.Err, uploader.UploadPartNotSeq) {
|
||||
// 分片乱序错误
|
||||
if ee := utu.amendFileUploadPartNum(); ee != nil {
|
||||
// 修正分片乱序失败,先令上传任务直接失败
|
||||
logger.Verboseln("WARNING! amend uploaded parts num failed")
|
||||
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: ee,
|
||||
ResultCode: 0,
|
||||
ResultMessage: "",
|
||||
Extra: nil,
|
||||
}
|
||||
return uploadResult
|
||||
}
|
||||
goto stepUploadUpload
|
||||
}
|
||||
if errors.Is(uploadResult.Err, uploader.UploadNoSuchUpload) {
|
||||
// 上传任务过期
|
||||
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 {
|
||||
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
|
||||
goto StepUploadPrepareUpload
|
||||
}
|
||||
}
|
||||
}
|
||||
return uploadResult
|
||||
}
|
||||
|
||||
// amendFileUploadPartNum 修正文件分片上传顺序错误
|
||||
func (utu *UploadTaskUnit) amendFileUploadPartNum() error {
|
||||
if utu.LocalFileChecksum.LocalFileMeta.UploadOpEntity == nil || utu.state == nil {
|
||||
return nil
|
||||
}
|
||||
logger.Verbosef("adjust the uploaded parts num error\n")
|
||||
// 分片出现乱序
|
||||
// 获取的已上传分片信息,修正正确的分片顺序
|
||||
uploadedParts, uper := utu.PanClient.OpenapiPanClient().GetUploadedPartInfoAllItem(&aliyunpan.GetUploadedPartsParam{
|
||||
DriveId: utu.LocalFileChecksum.LocalFileMeta.UploadOpEntity.DriveId,
|
||||
FileId: utu.LocalFileChecksum.LocalFileMeta.UploadOpEntity.FileId,
|
||||
UploadId: utu.LocalFileChecksum.LocalFileMeta.UploadOpEntity.UploadId,
|
||||
})
|
||||
if uper != nil {
|
||||
logger.Verbosef("get uploaded parts info error: %+v\n", uper)
|
||||
return uper
|
||||
}
|
||||
// 获取最后上传的分片编号
|
||||
lastUploadedPartNum := -1
|
||||
if len(uploadedParts.UploadedParts) > 0 {
|
||||
lastUploadedPartNum = uploadedParts.UploadedParts[len(uploadedParts.UploadedParts)-1].PartNumber
|
||||
} else {
|
||||
logger.Verbosef("get uploaded parts list is empty\n")
|
||||
return errors.New("uploaded parts list is empty")
|
||||
}
|
||||
// 修正分片上传的标识
|
||||
if lastUploadedPartNum > 0 {
|
||||
logger.Verbosef("get the right uploaded parts num: %d\n", lastUploadedPartNum)
|
||||
for _, w := range utu.state.BlockList {
|
||||
if (w.ID + 1) <= lastUploadedPartNum { // 分片的编号从1开始,BlockList的id是从0开始
|
||||
w.UploadDone = true
|
||||
} else {
|
||||
w.UploadDone = false
|
||||
}
|
||||
}
|
||||
}
|
||||
return 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)
|
||||
@@ -94,10 +97,3 @@ func CalcFilePreHash(filePath string) string {
|
||||
hashCode := hex.EncodeToString(shaBytes)
|
||||
return strings.ToUpper(hashCode)
|
||||
}
|
||||
|
||||
// UnixTime2LocalFormatStr 时间戳转换为东8区时间字符串
|
||||
func UnixTime2LocalFormatStr(unixTime int64) string {
|
||||
t := time.Unix(unixTime, 0)
|
||||
cz := time.FixedZone("CST", 8*3600) // 东8区
|
||||
return t.In(cz).Format("2006-01-02T15:04:05.000Z")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package global
|
||||
|
||||
type (
|
||||
FileSourceType string
|
||||
)
|
||||
|
||||
const (
|
||||
// FileSource 文件,包括:资源盘文件、备份盘文件
|
||||
FileSource FileSourceType = "file"
|
||||
// AlbumSource 相册
|
||||
AlbumSource FileSourceType = "album"
|
||||
)
|
||||
@@ -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[:])
|
||||
}
|
||||
|
||||
@@ -506,6 +506,8 @@ func (f *FileActionTask) uploadFile(ctx context.Context) error {
|
||||
BlockSize: f.syncItem.UploadBlockSize,
|
||||
ProofCode: proofCode,
|
||||
ProofVersion: "v1",
|
||||
LocalCreatedAt: utils.UnixTime2LocalFormatStr(localFile.ModTime),
|
||||
LocalModifiedAt: utils.UnixTime2LocalFormatStr(localFile.ModTime),
|
||||
}
|
||||
if uploadOpEntity, err := f.panClient.OpenapiPanClient().CreateUploadFile(appCreateUploadFileParam); err != nil {
|
||||
logger.Verbosef("创建云盘上传任务失败: %s\n", targetPanFilePath)
|
||||
@@ -526,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,
|
||||
|
||||
@@ -50,6 +50,8 @@ type (
|
||||
Priority SyncPriorityOption `json:"-"`
|
||||
// LastSyncTime 上一次同步时间
|
||||
LastSyncTime string `json:"lastSyncTime"`
|
||||
// ScanTimeInterval 扫描文件时间间隔,单位秒
|
||||
ScanTimeInterval int64 `json:"-"`
|
||||
|
||||
syncDbFolderPath string
|
||||
localFileDb LocalSyncDb
|
||||
@@ -243,8 +245,6 @@ func (t *SyncTask) Start() error {
|
||||
go t.scanLocalFile(t.ctx)
|
||||
} else if t.Mode == Download {
|
||||
go t.scanPanFile(t.ctx)
|
||||
} else if t.Mode == SyncTwoWay {
|
||||
return fmt.Errorf("异常:暂不支持该模式。")
|
||||
} else {
|
||||
return fmt.Errorf("异常:暂不支持该模式。")
|
||||
}
|
||||
@@ -497,7 +497,7 @@ func (t *SyncTask) scanLocalFile(ctx context.Context) {
|
||||
fileInfo: rootFolder,
|
||||
path: t.LocalFolderPath,
|
||||
})
|
||||
delayTimeCount = TimeSecondsOfOneMinute
|
||||
delayTimeCount = t.ScanTimeInterval
|
||||
continue
|
||||
}
|
||||
item := obj.(*folderItem)
|
||||
@@ -718,7 +718,7 @@ func (t *SyncTask) scanPanFile(ctx context.Context) {
|
||||
|
||||
// 无限循环模式,继续下一次扫描
|
||||
folderQueue.Push(rootPanFile)
|
||||
delayTimeCount = TimeSecondsOfOneMinute
|
||||
delayTimeCount = t.ScanTimeInterval
|
||||
continue
|
||||
}
|
||||
item := obj.(*aliyunpan.FileEntity)
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ func (m *SyncTaskManager) ConfigFilePath() string {
|
||||
}
|
||||
|
||||
// Start 启动同步进程
|
||||
func (m *SyncTaskManager) Start(tasks []*SyncTask, cycleMode CycleMode) (bool, error) {
|
||||
func (m *SyncTaskManager) Start(tasks []*SyncTask, cycleMode CycleMode, scanTimeInterval int64) (bool, error) {
|
||||
if tasks != nil && len(tasks) > 0 {
|
||||
m.syncDriveConfig = &SyncDriveConfig{
|
||||
ConfigVer: "1.0",
|
||||
@@ -141,6 +141,8 @@ func (m *SyncTaskManager) Start(tasks []*SyncTask, cycleMode CycleMode) (bool, e
|
||||
}
|
||||
// cycle mode
|
||||
task.CycleModeType = cycleMode
|
||||
// scan time
|
||||
task.ScanTimeInterval = scanTimeInterval
|
||||
|
||||
// check driveId
|
||||
if strings.ToLower(task.DriveName) == "backup" {
|
||||
@@ -233,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)
|
||||
}
|
||||
+168
-5
@@ -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,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 去除目录的前缀
|
||||
@@ -111,9 +114,22 @@ func TriggerOnSync(f func()) {
|
||||
}
|
||||
|
||||
func ParseVersionNum(versionStr string) int {
|
||||
// 解析版本号字符串
|
||||
// 正式版本号:v0.3.5
|
||||
// 开发版本号:v0.3.5-dev
|
||||
versionStr = strings.ReplaceAll(versionStr, "-dev", "")
|
||||
versionStr = strings.ReplaceAll(versionStr, "v", "")
|
||||
|
||||
// 适配版本号:v0.3.5-1
|
||||
if strings.Contains(versionStr, "-") {
|
||||
versionStr = strings.Split(versionStr, "-")[0]
|
||||
}
|
||||
|
||||
// 解析版本号数值
|
||||
versionParts := strings.Split(versionStr, ".")
|
||||
if len(versionParts) < 3 {
|
||||
return 0
|
||||
}
|
||||
verNum := parseInt(versionParts[0])*1e4 + parseInt(versionParts[1])*1e2 + parseInt(versionParts[2])
|
||||
return verNum
|
||||
}
|
||||
@@ -281,3 +297,150 @@ func ResizeUploadBlockSize(fileSize, defaultBlockSize int64) int64 {
|
||||
sizeOfMB := int64(math.Ceil(float64(fileSize) / float64(aliyunpan.MaxPartNum) / 1024.0))
|
||||
return sizeOfMB * 1024
|
||||
}
|
||||
|
||||
// UnixTime2LocalFormatStr 时间戳转换为东8区时间字符串
|
||||
func UnixTime2LocalFormatStr(unixTime int64) string {
|
||||
t := time.Unix(unixTime, 0)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -51,3 +51,18 @@ func TestResizeUploadBlockSize_ReturnNewBlockSize(t *testing.T) {
|
||||
fileSize := int64(107374182400) // 100GB
|
||||
fmt.Println(ResizeUploadBlockSize(fileSize, 10*MB)) // 10737664 = 10486KB
|
||||
}
|
||||
func TestParseVersionNum(t *testing.T) {
|
||||
fmt.Println(ParseVersionNum("v1.3.55"))
|
||||
}
|
||||
|
||||
func TestParseVersionNum2(t *testing.T) {
|
||||
fmt.Println(ParseVersionNum("v0.3.10-dev"))
|
||||
}
|
||||
|
||||
func TestParseVersionNum3(t *testing.T) {
|
||||
fmt.Println(ParseVersionNum("v0.3.5-1"))
|
||||
}
|
||||
|
||||
func TestParseVersionNum4(t *testing.T) {
|
||||
fmt.Println(ParseVersionNum("v"))
|
||||
}
|
||||
|
||||
@@ -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.3"
|
||||
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-2024 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
|
||||
}
|
||||
|
||||
@@ -475,6 +400,12 @@ func main() {
|
||||
// 工具箱 tool
|
||||
command.CmdTool(),
|
||||
|
||||
// 分享文件/目录 share
|
||||
command.CmdShare(),
|
||||
|
||||
// 相簿
|
||||
command.CmdAlbum(),
|
||||
|
||||
// 显示命令历史
|
||||
{
|
||||
Name: "history",
|
||||
@@ -620,6 +551,67 @@ func main() {
|
||||
},
|
||||
},
|
||||
|
||||
// 显示程序环境变量
|
||||
{
|
||||
Name: "env",
|
||||
Usage: "显示程序环境变量",
|
||||
Description: ` ALIYUNPAN_CONFIG_DIR: 配置文件路径
|
||||
ALIYUNPAN_DOWNLOAD_DIR: 配置下载路径
|
||||
ALIYUNPAN_VERBOSE: 是否启用调试
|
||||
XDG_CONFIG_HOME: XDG配置主目录
|
||||
XDG_DOWNLOAD_DIR: XDG配置下载目录
|
||||
`,
|
||||
Category: "其他",
|
||||
Action: func(c *cli.Context) error {
|
||||
envStr := "%s=%s\n"
|
||||
envVar, ok := os.LookupEnv(config.EnvVerbose)
|
||||
if ok {
|
||||
if envVar == "1" {
|
||||
fmt.Printf(envStr, config.EnvVerbose, "1")
|
||||
} else {
|
||||
fmt.Printf(envStr, config.EnvVerbose, "0")
|
||||
}
|
||||
} else {
|
||||
fmt.Printf(envStr, config.EnvVerbose, "0")
|
||||
}
|
||||
|
||||
envVar, ok = os.LookupEnv(config.EnvConfigDir)
|
||||
if ok {
|
||||
fmt.Printf(envStr, config.EnvConfigDir, envVar)
|
||||
} else {
|
||||
fmt.Printf(envStr, config.EnvConfigDir, config.GetConfigDir())
|
||||
}
|
||||
|
||||
envVar, ok = os.LookupEnv(config.EnvDownloadDir)
|
||||
if ok {
|
||||
fmt.Printf(envStr, config.EnvDownloadDir, envVar)
|
||||
} else {
|
||||
fmt.Printf(envStr, config.EnvDownloadDir, config.GetDefaultDownloadDir())
|
||||
}
|
||||
|
||||
envVar, ok = os.LookupEnv("XDG_CONFIG_HOME")
|
||||
if ok {
|
||||
fmt.Printf(envStr, "XDG_CONFIG_HOME", envVar)
|
||||
} else {
|
||||
fmt.Printf(envStr, "XDG_CONFIG_HOME", "")
|
||||
}
|
||||
envVar, ok = os.LookupEnv("XDG_DOWNLOAD_DIR")
|
||||
if ok {
|
||||
fmt.Printf(envStr, "XDG_DOWNLOAD_DIR", envVar)
|
||||
} else {
|
||||
fmt.Printf(envStr, "XDG_DOWNLOAD_DIR", "")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
|
||||
// 当前本地工作目录
|
||||
command_local.CmdLocalPwd(),
|
||||
// 切换本地工作目录
|
||||
command_local.CmdLocalCd(),
|
||||
// 展示本地目录文件列表
|
||||
command_local.CmdLocalLs(),
|
||||
|
||||
// 调试用 debug
|
||||
//{
|
||||
// Name: "debug",
|
||||
@@ -627,11 +619,8 @@ func main() {
|
||||
// Usage: "开发调试用",
|
||||
// Description: "",
|
||||
// Category: "debug",
|
||||
// Before: cmder.ReloadConfigFunc,
|
||||
// Before: command.ReloadConfigFunc,
|
||||
// Action: func(c *cli.Context) error {
|
||||
// os.Setenv(config.EnvVerbose, "1")
|
||||
// logger.IsVerbose = true
|
||||
// fmt.Println("显示调试日志", logger.IsVerbose)
|
||||
// return nil
|
||||
// },
|
||||
// Flags: []cli.Flag{
|
||||
@@ -655,8 +644,8 @@ func main() {
|
||||
// 备份盘和资源库之间拷贝文件 xcp
|
||||
command.CmdXcp(),
|
||||
|
||||
// 分享文件/目录 share
|
||||
command.CmdShare(),
|
||||
// 分享文件/目录 sharew,web端接口的分享功能
|
||||
command.CmdShareWeb(),
|
||||
|
||||
// 保存分享文件/目录 save
|
||||
command.CmdSave(),
|
||||
@@ -673,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": 3,
|
||||
"Minor": 4,
|
||||
"Patch": 0,
|
||||
"Build": 0
|
||||
},
|
||||
"ProductVersion": {
|
||||
"Major": 0,
|
||||
"Minor": 3,
|
||||
"Patch": 3,
|
||||
"Minor": 4,
|
||||
"Patch": 0,
|
||||
"Build": 0
|
||||
},
|
||||
"FileFlagsMask": "3f",
|
||||
@@ -22,14 +22,14 @@
|
||||
"Comments": "",
|
||||
"CompanyName": "tickstep",
|
||||
"FileDescription": "阿里云盘客户端",
|
||||
"FileVersion": "v0.3.3",
|
||||
"FileVersion": "v0.4.0",
|
||||
"InternalName": "",
|
||||
"LegalCopyright": "© 2021-2024 tickstep.",
|
||||
"LegalCopyright": "© 2021-2026 tickstep.",
|
||||
"LegalTrademarks": "",
|
||||
"OriginalFilename": "",
|
||||
"PrivateBuild": "",
|
||||
"ProductName": "aliyunpan",
|
||||
"ProductVersion": "v0.3.3",
|
||||
"ProductVersion": "v0.4.0",
|
||||
"SpecialBuild": ""
|
||||
},
|
||||
"VarFileInfo": {
|
||||
|
||||
Reference in New Issue
Block a user